<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2">
  <meta name="theme-color" content="#222">
  <meta name="generator" content="Hexo 4.2.1">
  <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon-next.png">
  <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32-next.png">
  <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16-next.png">
  <link rel="mask-icon" href="/images/safari-pinned-tab.svg" color="#222">
  <link rel="stylesheet" href="/css/main.css">
  <link rel="stylesheet" href="/lib/font-awesome/css/all.min.css">
  <link rel="stylesheet" href="/lib/pace/pace-theme-minimal.min.css">
  <script src="/lib/pace/pace.min.js"></script>
  <script id="hexo-configurations">
    var NexT = window.NexT ||
    {};
    var CONFIG = {
      "hostname": "cuiqingcai.com",
      "root": "/",
      "scheme": "Pisces",
      "version": "7.8.0",
      "exturl": false,
      "sidebar":
      {
        "position": "right",
        "width": 360,
        "display": "post",
        "padding": 18,
        "offset": 12,
        "onmobile": false,
        "widgets": [
          {
            "type": "image",
            "name": "阿布云",
            "enable": false,
            "url": "https://www.abuyun.com/http-proxy/introduce.html",
            "src": "https://qiniu.cuiqingcai.com/88au8.jpg",
            "width": "100%"
      },
          {
            "type": "image",
            "name": "天验",
            "enable": true,
            "url": "https://tutorial.lengyue.video/?coupon=12ef4b1a-a3db-11ea-bb37-0242ac130002_cqx_850",
            "src": "https://qiniu.cuiqingcai.com/bco2a.png",
            "width": "100%"
      },
          {
            "type": "image",
            "name": "华为云",
            "enable": false,
            "url": "https://activity.huaweicloud.com/2020_618_promotion/index.html?bpName=5f9f98a29e2c40b780c1793086f29fe2&bindType=1&salesID=wangyubei",
            "src": "https://qiniu.cuiqingcai.com/y42ik.jpg",
            "width": "100%"
      },
          {
            "type": "image",
            "name": "张小鸡",
            "enable": false,
            "url": "http://www.zxiaoji.com/",
            "src": "https://qiniu.cuiqingcai.com/fm72f.png",
            "width": "100%"
      },
          {
            "type": "image",
            "name": "Luminati",
            "src": "https://qiniu.cuiqingcai.com/ikkq9.jpg",
            "url": "https://luminati-china.io/?affiliate=ref_5fbbaaa9647883f5c6f77095",
            "width": "100%",
            "enable": false
      },
          {
            "type": "image",
            "name": "IPIDEA",
            "url": "http://www.ipidea.net/?utm-source=cqc&utm-keyword=?cqc",
            "src": "https://qiniu.cuiqingcai.com/0ywun.png",
            "width": "100%",
            "enable": true
      },
          {
            "type": "tags",
            "name": "标签云",
            "enable": true
      },
          {
            "type": "categories",
            "name": "分类",
            "enable": true
      },
          {
            "type": "friends",
            "name": "友情链接",
            "enable": true
      },
          {
            "type": "hot",
            "name": "猜你喜欢",
            "enable": true
      }]
      },
      "copycode":
      {
        "enable": true,
        "show_result": true,
        "style": "mac"
      },
      "back2top":
      {
        "enable": true,
        "sidebar": false,
        "scrollpercent": true
      },
      "bookmark":
      {
        "enable": false,
        "color": "#222",
        "save": "auto"
      },
      "fancybox": false,
      "mediumzoom": false,
      "lazyload": false,
      "pangu": true,
      "comments":
      {
        "style": "tabs",
        "active": "gitalk",
        "storage": true,
        "lazyload": false,
        "nav": null,
        "activeClass": "gitalk"
      },
      "algolia":
      {
        "hits":
        {
          "per_page": 10
        },
        "labels":
        {
          "input_placeholder": "Search for Posts",
          "hits_empty": "We didn't find any results for the search: ${query}",
          "hits_stats": "${hits} results found in ${time} ms"
        }
      },
      "localsearch":
      {
        "enable": true,
        "trigger": "auto",
        "top_n_per_article": 10,
        "unescape": false,
        "preload": false
      },
      "motion":
      {
        "enable": false,
        "async": false,
        "transition":
        {
          "post_block": "bounceDownIn",
          "post_header": "slideDownIn",
          "post_body": "slideDownIn",
          "coll_header": "slideLeftIn",
          "sidebar": "slideUpIn"
        }
      },
      "path": "search.xml"
    };

  </script>
  <meta name="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
  <meta property="og:type" content="website">
  <meta property="og:title" content="静觅">
  <meta property="og:url" content="https://cuiqingcai.com/page/11/index.html">
  <meta property="og:site_name" content="静觅">
  <meta property="og:description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
  <meta property="og:locale" content="zh_CN">
  <meta property="article:author" content="崔庆才">
  <meta property="article:tag" content="崔庆才">
  <meta property="article:tag" content="静觅">
  <meta property="article:tag" content="PHP">
  <meta property="article:tag" content="Java">
  <meta property="article:tag" content="Python">
  <meta property="article:tag" content="Spider">
  <meta property="article:tag" content="爬虫">
  <meta property="article:tag" content="Web">
  <meta property="article:tag" content="Kubernetes">
  <meta property="article:tag" content="深度学习">
  <meta property="article:tag" content="机器学习">
  <meta property="article:tag" content="数据分析">
  <meta property="article:tag" content="网络">
  <meta property="article:tag" content="IT">
  <meta property="article:tag" content="技术">
  <meta property="article:tag" content="博客">
  <meta name="twitter:card" content="summary">
  <link rel="canonical" href="https://cuiqingcai.com/page/11/">
  <script id="page-configurations">
    // https://hexo.io/docs/variables.html
    CONFIG.page = {
      sidebar: "",
      isHome: true,
      isPost: false,
      lang: 'zh-CN'
    };

  </script>
  <title>静觅丨崔庆才的个人站点</title>
  <meta name="google-site-verification" content="p_bIcnvirkFzG2dYKuNDivKD8-STet5W7D-01woA2fc" />
  <noscript>
    <style>
      .use-motion .brand,
      .use-motion .menu-item,
      .sidebar-inner,
      .use-motion .post-block,
      .use-motion .pagination,
      .use-motion .comments,
      .use-motion .post-header,
      .use-motion .post-body,
      .use-motion .collection-header
      {
        opacity: initial;
      }

      .use-motion .site-title,
      .use-motion .site-subtitle
      {
        opacity: initial;
        top: initial;
      }

      .use-motion .logo-line-before i
      {
        left: initial;
      }

      .use-motion .logo-line-after i
      {
        right: initial;
      }

    </style>
  </noscript>
  <link rel="alternate" href="/atom.xml" title="静觅" type="application/atom+xml">
</head>

<body itemscope itemtype="http://schema.org/WebPage">
  <div class="container">
    <div class="headband"></div>
    <header class="header" itemscope itemtype="http://schema.org/WPHeader">
      <div class="header-inner">
        <div class="site-brand-container">
          <div class="site-nav-toggle">
            <div class="toggle" aria-label="切换导航栏">
              <span class="toggle-line toggle-line-first"></span>
              <span class="toggle-line toggle-line-middle"></span>
              <span class="toggle-line toggle-line-last"></span>
            </div>
          </div>
          <div class="site-meta">
            <a href="/" class="brand" rel="start">
              <span class="logo-line-before"><i></i></span>
              <h1 class="site-title">静觅 <span class="site-subtitle"> 崔庆才的个人站点 </span>
              </h1>
              <span class="logo-line-after"><i></i></span>
            </a>
          </div>
          <div class="site-nav-right">
            <div class="toggle popup-trigger">
              <i class="fa fa-search fa-fw fa-lg"></i>
            </div>
          </div>
        </div>
        <nav class="site-nav">
          <ul id="menu" class="main-menu menu">
            <li class="menu-item menu-item-home">
              <a href="/" rel="section">首页</a>
            </li>
            <li class="menu-item menu-item-archives">
              <a href="/archives/" rel="section">文章列表</a>
            </li>
            <li class="menu-item menu-item-tags">
              <a href="/tags/" rel="section">文章标签</a>
            </li>
            <li class="menu-item menu-item-categories">
              <a href="/categories/" rel="section">文章分类</a>
            </li>
            <li class="menu-item menu-item-about">
              <a href="/about/" rel="section">关于博主</a>
            </li>
            <li class="menu-item menu-item-message">
              <a href="/message/" rel="section">给我留言</a>
            </li>
            <li class="menu-item menu-item-search">
              <a role="button" class="popup-trigger">搜索 </a>
            </li>
          </ul>
        </nav>
        <div class="search-pop-overlay">
          <div class="popup search-popup">
            <div class="search-header">
              <span class="search-icon">
                <i class="fa fa-search"></i>
              </span>
              <div class="search-input-container">
                <input autocomplete="off" autocapitalize="off" placeholder="搜索..." spellcheck="false" type="search" class="search-input">
              </div>
              <span class="popup-btn-close">
                <i class="fa fa-times-circle"></i>
              </span>
            </div>
            <div id="search-result">
              <div id="no-result">
                <i class="fa fa-spinner fa-pulse fa-5x fa-fw"></i>
              </div>
            </div>
          </div>
        </div>
      </div>
    </header>
    <div class="back-to-top">
      <i class="fa fa-arrow-up"></i>
      <span>0%</span>
    </div>
    <div class="reading-progress-bar"></div>
    <main class="main">
      <div class="main-inner">
        <div class="content-wrap">
          <div class="content index posts-expand">
            <div class="carousel">
              <div id="wowslider-container">
                <div class="ws_images">
                  <ul>
                    <li><a target="_blank" href="https://cuiqingcai.com/5052.html"><img title="Python3网络爬虫开发实战教程" src="https://qiniu.cuiqingcai.com/ipy96.jpg" /></a></li>
                    <li><a target="_blank" href="https://t.lagou.com/fRCBRsRCSN6FA"><img title="52讲轻松搞定网络爬虫" src="https://qiniu.cuiqingcai.com/fqq5e.png" /></a></li>
                    <li><a target="_blank" href="https://brightdata.grsm.io/cuiqingcai"><img title="亮网络解锁器" src="https://qiniu.cuiqingcai.com/6qnb7.png" /></a></li>
                    <li><a target="_blank" href="https://cuiqingcai.com/4320.html"><img title="Python3网络爬虫开发视频教程" src="https://qiniu.cuiqingcai.com/bjrny.jpg" /></a></li>
                    <li><a target="_blank" href="https://cuiqingcai.com/5094.html"><img title="爬虫代理哪家强？十大付费代理详细对比评测出炉！" src="https://qiniu.cuiqingcai.com/nifs6.jpg" /></a></li>
                  </ul>
                </div>
                <div class="ws_thumbs">
                  <div>
                    <a target="_blank" href="#"><img src="https://qiniu.cuiqingcai.com/ipy96.jpg" /></a>
                    <a target="_blank" href="#"><img src="https://qiniu.cuiqingcai.com/fqq5e.png" /></a>
                    <a target="_blank" href="#"><img src="https://qiniu.cuiqingcai.com/6qnb7.png" /></a>
                    <a target="_blank" href="#"><img src="https://qiniu.cuiqingcai.com/bjrny.jpg" /></a>
                    <a target="_blank" href="#"><img src="https://qiniu.cuiqingcai.com/nifs6.jpg" /></a>
                  </div>
                </div>
                <div class="ws_shadow"></div>
              </div>
            </div>
            <link rel="stylesheet" href="/lib/wowslide/slide.css">
            <script src="/lib/wowslide/jquery.min.js"></script>
            <script src="/lib/wowslide/slider.js"></script>
            <script>
              jQuery("#wowslider-container").wowSlider(
              {
                effect: "cube",
                prev: "",
                next: "",
                duration: 20 * 100,
                delay: 20 * 100,
                width: 716,
                height: 297,
                autoPlay: true,
                playPause: true,
                stopOnHover: false,
                loop: false,
                bullets: 0,
                caption: true,
                captionEffect: "slide",
                controls: true,
                onBeforeStep: 0,
                images: 0
              });

            </script>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8320.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8320.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 12.3-pyspider 用法详解</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="12-3-pyspider-用法详解"><a href="#12-3-pyspider-用法详解" class="headerlink" title="12.3 pyspider 用法详解"></a>12.3 pyspider 用法详解</h1>
                  <p>前面我们了解了 pyspider 的基本用法，我们通过非常少的代码和便捷的可视化操作就完成了一个爬虫的编写，本节我们来总结一下它的详细用法。</p>
                  <h3 id="1-命令行"><a href="#1-命令行" class="headerlink" title="1. 命令行"></a>1. 命令行</h3>
                  <p>上面的实例通过如下命令启动 pyspider：</p>
                  <figure class="highlight ada">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">pyspider <span class="keyword">all</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>命令行还有很多可配制参数，完整的命令行结构如下所示：</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="selector-tag">pyspider</span> <span class="selector-attr">[OPTIONS]</span> <span class="selector-tag">COMMAND</span> <span class="selector-attr">[ARGS]</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>其中，OPTIONS 为可选参数，它可以指定如下参数。</p>
                  <figure class="highlight ada">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">Options:</span><br><span class="line">  -c, <span class="comment">--config FILENAME    指定配置文件名称</span></span><br><span class="line">  <span class="comment">--logging-config TEXT    日志配置文件名称，默认: pyspider/pyspider/logging.conf</span></span><br><span class="line">  <span class="comment">--debug                  开启调试模式</span></span><br><span class="line">  <span class="comment">--queue-maxsize INTEGER  队列的最大长度</span></span><br><span class="line">  <span class="comment">--taskdb TEXT            taskdb 的数据库连接字符串，默认: sqlite</span></span><br><span class="line">  <span class="comment">--projectdb TEXT         projectdb 的数据库连接字符串，默认: sqlite</span></span><br><span class="line">  <span class="comment">--resultdb TEXT          resultdb 的数据库连接字符串，默认: sqlite</span></span><br><span class="line">  <span class="comment">--message-queue TEXT     消息队列连接字符串，默认: multiprocessing.Queue</span></span><br><span class="line">  <span class="comment">--phantomjs-proxy TEXT   PhantomJS 使用的代理，ip:port 的形式</span></span><br><span class="line">  <span class="comment">--data-path TEXT         数据库存放的路径</span></span><br><span class="line">  <span class="comment">--version                pyspider 的版本</span></span><br><span class="line">  <span class="comment">--help                   显示帮助信息</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>例如，-c 可以指定配置文件的名称，这是一个常用的配置，配置文件的样例结构如下所示：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"taskdb"</span>: <span class="string">"mysql+taskdb://username:password@host:port/taskdb"</span>,</span><br><span class="line">  <span class="attr">"projectdb"</span>: <span class="string">"mysql+projectdb://username:password@host:port/projectdb"</span>,</span><br><span class="line">  <span class="attr">"resultdb"</span>: <span class="string">"mysql+resultdb://username:password@host:port/resultdb"</span>,</span><br><span class="line">  <span class="attr">"message_queue"</span>: <span class="string">"amqp://username:password@host:port/%2F"</span>,</span><br><span class="line">  <span class="attr">"webui"</span>: &#123;</span><br><span class="line">    <span class="attr">"username"</span>: <span class="string">"some_name"</span>,</span><br><span class="line">    <span class="attr">"password"</span>: <span class="string">"some_passwd"</span>,</span><br><span class="line">    <span class="attr">"need-auth"</span>: <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>如果要配置 pyspider WebUI 的访问认证，可以新建一个 pyspider.json，内容如下所示：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"webui"</span>: &#123;</span><br><span class="line">    <span class="attr">"username"</span>: <span class="string">"root"</span>,</span><br><span class="line">    <span class="attr">"password"</span>: <span class="string">"123456"</span>,</span><br><span class="line">    <span class="attr">"need-auth"</span>: <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我们通过在启动时指定配置文件来配置 pyspider WebUI 的访问认证，用户名为 root，密码为 123456，命令如下所示：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">pyspider -c pyspider.json <span class="keyword">all</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行之后打开：<a href="http://localhost:5000/" target="_blank" rel="noopener">http://localhost:5000/</a>，页面如 12-26 所示： <img src="https://qiniu.cuiqingcai.com/2019-11-27-033806.png" alt=""> 图 12-26 运行页面 也可以单独运行 pyspider 的某一个组件。 运行 Scheduler 的命令如下所示：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">pyspider<span class="built_in"> scheduler </span>[OPTIONS]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行时也可以指定各种配置，参数如下所示：</p>
                  <figure class="highlight brainfuck">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment">Options:</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">xmlrpc</span> <span class="comment">/</span>--<span class="comment">no</span><span class="literal">-</span><span class="comment">xmlrpc</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">xmlrpc</span><span class="literal">-</span><span class="comment">host</span> <span class="comment">TEXT</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">xmlrpc</span><span class="literal">-</span><span class="comment">port</span> <span class="comment">INTEGER</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">inqueue</span><span class="literal">-</span><span class="comment">limit</span> <span class="comment">INTEGER</span>  <span class="comment">任务队列的最大长度，如果满了则新的任务会被忽略</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">delete</span><span class="literal">-</span><span class="comment">time</span> <span class="comment">INTEGER</span>    <span class="comment">设置为</span> <span class="comment">delete</span> <span class="comment">标记之前的删除时间</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">active</span><span class="literal">-</span><span class="comment">tasks</span> <span class="comment">INTEGER</span>   <span class="comment">当前活跃任务数量配置</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">loop</span><span class="literal">-</span><span class="comment">limit</span> <span class="comment">INTEGER</span>     <span class="comment">单轮最多调度的任务数量</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">scheduler</span><span class="literal">-</span><span class="comment">cls</span> <span class="comment">TEXT</span>     <span class="comment">Scheduler</span> <span class="comment">使用的类</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">help</span>                   <span class="comment">显示帮助信息</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行 Fetcher 的命令如下所示：</p>
                  <figure class="highlight apache">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">pyspider</span> fetcher<span class="meta"> [OPTIONS]</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>参数配置如下所示：</p>
                  <figure class="highlight brainfuck">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment">Options:</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">xmlrpc</span> <span class="comment">/</span>--<span class="comment">no</span><span class="literal">-</span><span class="comment">xmlrpc</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">xmlrpc</span><span class="literal">-</span><span class="comment">host</span> <span class="comment">TEXT</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">xmlrpc</span><span class="literal">-</span><span class="comment">port</span> <span class="comment">INTEGER</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">poolsize</span> <span class="comment">INTEGER</span>      <span class="comment">同时请求的个数</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">proxy</span> <span class="comment">TEXT</span>            <span class="comment">使用的代理</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">user</span><span class="literal">-</span><span class="comment">agent</span> <span class="comment">TEXT</span>       <span class="comment">使用的</span> <span class="comment">User</span><span class="literal">-</span><span class="comment">Agent</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">timeout</span> <span class="comment">TEXT</span>          <span class="comment">超时时间</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">fetcher</span><span class="literal">-</span><span class="comment">cls</span> <span class="comment">TEXT</span>      <span class="comment">Fetcher</span> <span class="comment">使用的类</span></span><br><span class="line"><span class="comment"></span>  --<span class="comment">help</span>                  <span class="comment">显示帮助信息</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行 Processer 的命令如下所示：</p>
                  <figure class="highlight apache">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">pyspider</span> processor<span class="meta"> [OPTIONS]</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>参数配置如下所示：</p>
                  <figure class="highlight ada">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">Options:</span><br><span class="line">  <span class="comment">--processor-cls TEXT  Processor 使用的类</span></span><br><span class="line">  <span class="comment">--help                显示帮助信息</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行 WebUI 的命令如下所示：</p>
                  <figure class="highlight apache">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">pyspider</span> webui<span class="meta"> [OPTIONS]</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>参数配置如下所示：</p>
                  <figure class="highlight ada">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">Options:</span><br><span class="line">  <span class="comment">--host TEXT            运行地址</span></span><br><span class="line">  <span class="comment">--port INTEGER         运行端口</span></span><br><span class="line">  <span class="comment">--cdn TEXT             JS 和 CSS 的 CDN 服务器</span></span><br><span class="line">  <span class="comment">--scheduler-rpc TEXT   Scheduler 的 xmlrpc 路径</span></span><br><span class="line">  <span class="comment">--fetcher-rpc TEXT     Fetcher 的 xmlrpc 路径</span></span><br><span class="line">  <span class="comment">--max-rate FLOAT       每个项目最大的 rate 值</span></span><br><span class="line">  <span class="comment">--max-burst FLOAT      每个项目最大的 burst 值</span></span><br><span class="line">  <span class="comment">--username TEXT        Auth 验证的用户名</span></span><br><span class="line">  <span class="comment">--password TEXT        Auth 验证的密码</span></span><br><span class="line">  <span class="comment">--need-auth            是否需要验证</span></span><br><span class="line">  <span class="comment">--webui-instance TEXT  运行时使用的 Flask 应用</span></span><br><span class="line">  <span class="comment">--help                 显示帮助信息</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里的配置和前面提到的配置文件参数是相同的。如果想要改变 WebUI 的端口为 5001，单独运行如下命令：</p>
                  <figure class="highlight ada">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">pyspider webui <span class="comment">--port 5001</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>或者可以将端口配置到 JSON 文件中，配置如下所示：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"webui"</span>: &#123;<span class="attr">"port"</span>: <span class="number">5001</span>&#125;</span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>使用如下命令启动同样可以达到相同的效果：</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="selector-tag">pyspider</span> <span class="selector-tag">-c</span> <span class="selector-tag">pyspider</span><span class="selector-class">.json</span> <span class="selector-tag">webui</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样就可以在 5001 端口上运行 WebUI 了。</p>
                  <h3 id="2-crawl-方法"><a href="#2-crawl-方法" class="headerlink" title="2. crawl() 方法"></a>2. crawl() 方法</h3>
                  <p>在前面的例子中，我们使用 crawl() 方法实现了新请求的生成，但是只指定了 URL 和 Callback。这里将详细介绍一下 crawl() 方法的参数配置。</p>
                  <h4 id="url"><a href="#url" class="headerlink" title="url"></a>url</h4>
                  <p>url 是爬取时的 URL，可以定义为单个 URL 字符串，也可以定义成 URL 列表。</p>
                  <h4 id="callback"><a href="#callback" class="headerlink" title="callback"></a>callback</h4>
                  <p>callback 是回调函数，指定了该 URL 对应的响应内容用哪个方法来解析，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">on_start</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://scrapy.org/'</span>, callback=<span class="keyword">self</span>.index_page)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里指定了 callback 为 index_page，就代表爬取 <a href="http://scrapy.org/" target="_blank" rel="noopener">http://scrapy.org/</a> 链接得到的响应会用 index_page() 方法来解析。 index_page() 方法的第一个参数是响应对象，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">index_page</span><span class="params">(<span class="keyword">self</span>, response)</span></span><span class="symbol">:</span></span><br><span class="line">    pass</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>方法中的 response 参数就是请求上述 URL 得到的响应对象，我们可以直接在 index_page() 方法中实现页面的解析。</p>
                  <h4 id="age"><a href="#age" class="headerlink" title="age"></a>age</h4>
                  <p>age 是任务的有效时间。如果某个任务在有效时间内且已经被执行，则它不会重复执行，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">on_start</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://www.example.org/'</span>, callback=<span class="keyword">self</span>.callback,</span><br><span class="line">               age=<span class="number">10</span>*<span class="number">24</span>*<span class="number">60</span>*<span class="number">60</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>或者可以这样设置：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">@config(age=<span class="number">10</span> * <span class="number">24</span> * <span class="number">60</span> * <span class="number">60</span>)</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">callback</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    pass</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>默认的有效时间为 10 天。</p>
                  <h4 id="priority"><a href="#priority" class="headerlink" title="priority"></a>priority</h4>
                  <p>priority 是爬取任务的优先级，其值默认是 0，priority 的数值越大，对应的请求会越优先被调度，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">index_page</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://www.example.org/page.html'</span>, callback=<span class="keyword">self</span>.index_page)</span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://www.example.org/233.html'</span>, callback=<span class="keyword">self</span>.detail_page,</span><br><span class="line">               priority=<span class="number">1</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>第二个任务会优先调用，233.html 这个链接优先爬取。</p>
                  <h4 id="exetime"><a href="#exetime" class="headerlink" title="exetime"></a>exetime</h4>
                  <p>exetime 参数可以设置定时任务，其值是时间戳，默认是 0，即代表立即执行，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">import time</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">on_start</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://www.example.org/'</span>, callback=<span class="keyword">self</span>.callback,</span><br><span class="line">               exetime=time.time()+<span class="number">30</span>*<span class="number">60</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样该任务会在 30 分钟之后执行。</p>
                  <h4 id="retries"><a href="#retries" class="headerlink" title="retries"></a>retries</h4>
                  <p>retries 可以定义重试次数，其值默认是 3。</p>
                  <h4 id="itag"><a href="#itag" class="headerlink" title="itag"></a>itag</h4>
                  <p>itag 参数设置判定网页是否发生变化的节点值，在爬取时会判定次当前节点是否和上次爬取到的节点相同。如果节点相同，则证明页面没有更新，就不会重复爬取，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">index_page</span><span class="params">(<span class="keyword">self</span>, response)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">for</span> item <span class="keyword">in</span> response.doc(<span class="string">'.item'</span>).items()<span class="symbol">:</span></span><br><span class="line">        <span class="keyword">self</span>.crawl(item.find(<span class="string">'a'</span>).attr.url, callback=<span class="keyword">self</span>.detail_page,</span><br><span class="line">                   itag=item.find(<span class="string">'.update-time'</span>).text())</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里设置了更新时间这个节点的值为 itag，在下次爬取时就会首先检测这个值有没有发生变化，如果没有变化，则不再重复爬取，否则执行爬取。</p>
                  <h4 id="auto-recrawl"><a href="#auto-recrawl" class="headerlink" title="auto_recrawl"></a>auto_recrawl</h4>
                  <p>当开启时，爬取任务在过期后会重新执行，循环时间即定义的 age 时间长度，如下所示：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def on_start(self):</span><br><span class="line">    self.crawl(<span class="string">'http://www.example.org/'</span>, <span class="attribute">callback</span>=self.callback,</span><br><span class="line">               <span class="attribute">age</span>=5*60*60, <span class="attribute">auto_recrawl</span>=<span class="literal">True</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里定义了 age 有效期为 5 小时，设置了 auto_recrawl 为 True，这样任务就会每 5 小时执行一次。</p>
                  <h4 id="method"><a href="#method" class="headerlink" title="method"></a>method</h4>
                  <p>method 是 HTTP 请求方式，它默认是 GET。如果想发起 POST 请求，可以将 method 设置为 POST。</p>
                  <h4 id="params"><a href="#params" class="headerlink" title="params"></a>params</h4>
                  <p>我们可以方便地使用 params 来定义 GET 请求参数，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">on_start</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://httpbin.org/get'</span>, callback=<span class="keyword">self</span>.callback,</span><br><span class="line">               params=&#123;<span class="string">'a'</span>: <span class="number">123</span>, <span class="string">'b'</span>: <span class="string">'c'</span>&#125;)</span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://httpbin.org/get?a=123&amp;b=c'</span>, callback=<span class="keyword">self</span>.callback)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里两个爬取任务是等价的。</p>
                  <h4 id="data"><a href="#data" class="headerlink" title="data"></a>data</h4>
                  <p>data 是 POST 表单数据。当请求方式为 POST 时，我们可以通过此参数传递表单数据，如下所示：</p>
                  <figure class="highlight oxygene">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def on_start(<span class="keyword">self</span>):</span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://httpbin.org/post'</span>, callback=<span class="keyword">self</span>.callback,</span><br><span class="line">               <span class="function"><span class="keyword">method</span>='<span class="title">POST</span>', <span class="title">data</span>=<span class="comment">&#123;'a': 123, 'b': 'c'&#125;</span>)</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h4 id="files"><a href="#files" class="headerlink" title="files"></a>files</h4>
                  <p>files 是上传的文件，需要指定文件名，如下所示：</p>
                  <figure class="highlight oxygene">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def on_start(<span class="keyword">self</span>):</span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://httpbin.org/post'</span>, callback=<span class="keyword">self</span>.callback,</span><br><span class="line">               <span class="function"><span class="keyword">method</span>='<span class="title">POST</span>', <span class="title">files</span>=<span class="comment">&#123;field: &#123;filename: 'content'&#125;</span>&#125;)</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h4 id="user-agent"><a href="#user-agent" class="headerlink" title="user_agent"></a>user_agent</h4>
                  <p>user_agent 是爬取使用的 User-Agent。</p>
                  <h4 id="headers"><a href="#headers" class="headerlink" title="headers"></a>headers</h4>
                  <p>headers 是爬取时使用的 Headers，即 Request Headers。</p>
                  <h4 id="cookies"><a href="#cookies" class="headerlink" title="cookies"></a>cookies</h4>
                  <p>cookies 是爬取时使用的 Cookies，为字典格式。</p>
                  <h4 id="connect-timeout"><a href="#connect-timeout" class="headerlink" title="connect_timeout"></a>connect_timeout</h4>
                  <p>connect_timeout 是在初始化连接时的最长等待时间，它默认是 20 秒。</p>
                  <h4 id="timeout"><a href="#timeout" class="headerlink" title="timeout"></a>timeout</h4>
                  <p>timeout 是抓取网页时的最长等待时间，它默认是 120 秒。</p>
                  <h4 id="allow-redirects"><a href="#allow-redirects" class="headerlink" title="allow_redirects"></a>allow_redirects</h4>
                  <p>allow_redirects 确定是否自动处理重定向，它默认是 True。</p>
                  <h4 id="validate-cert"><a href="#validate-cert" class="headerlink" title="validate_cert"></a>validate_cert</h4>
                  <p>validate_cert 确定是否验证证书，此选项对 HTTPS 请求有效，默认是 True。</p>
                  <h4 id="proxy"><a href="#proxy" class="headerlink" title="proxy"></a>proxy</h4>
                  <p>proxy 是爬取时使用的代理，它支持用户名密码的配置，格式为 username:password@hostname:port，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">on_start</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://httpbin.org/get'</span>, callback=<span class="keyword">self</span>.callback, proxy=<span class="string">'127.0.0.1:9743'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>也可以设置 craw_config 来实现全局配置，如下所示：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">class</span> <span class="symbol">Handler</span>(<span class="symbol">BaseHandler</span>):</span><br><span class="line">    <span class="symbol">crawl_config</span> = &#123;<span class="string">'proxy'</span>: <span class="string">'127.0.0.1:9743'</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h4 id="fetch-type"><a href="#fetch-type" class="headerlink" title="fetch_type"></a>fetch_type</h4>
                  <p>fetch_type 开启 PhantomJS 渲染。如果遇到 JavaScript 渲染的页面，指定此字段即可实现 PhantomJS 的对接，pyspider 将会使用 PhantomJS 进行网页的抓取，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">on_start</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'https://www.taobao.com'</span>, callback=<span class="keyword">self</span>.index_page, fetch_type=<span class="string">'js'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我们就可以实现淘宝页面的抓取了，得到的结果就是浏览器中看到的效果。</p>
                  <h4 id="js-script"><a href="#js-script" class="headerlink" title="js_script"></a>js_script</h4>
                  <p>js_script 是页面加载完毕后执行的 JavaScript 脚本，如下所示：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">on_start</span><span class="params">(self)</span>:</span></span><br><span class="line">    self.crawl(<span class="string">'http://www.example.org/'</span>, callback=self.callback,</span><br><span class="line">               fetch_type=<span class="string">'js'</span>, js_script=<span class="string">'''</span></span><br><span class="line"><span class="string">               function() &#123;window.scrollTo(0,document.body.scrollHeight);</span></span><br><span class="line"><span class="string">                   return 123;</span></span><br><span class="line"><span class="string">               &#125;</span></span><br><span class="line"><span class="string">               '''</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>页面加载成功后将执行页面混动的 JavaScript 代码，页面会下拉到最底部。</p>
                  <h4 id="js-run-at"><a href="#js-run-at" class="headerlink" title="js_run_at"></a>js_run_at</h4>
                  <p>js_run_at 代表 JavaScript 脚本运行的位置，是在页面节点开头还是结尾，默认是结尾，即 document-end。</p>
                  <h4 id="js-viewport-width-js-viewport-height"><a href="#js-viewport-width-js-viewport-height" class="headerlink" title="js_viewport_width/js_viewport_height"></a>js_viewport_width/js_viewport_height</h4>
                  <p>js_viewport_width/js_viewport_height 是 JavaScript 渲染页面时的窗口大小。</p>
                  <h4 id="load-images"><a href="#load-images" class="headerlink" title="load_images"></a>load_images</h4>
                  <p>load_images 在加载 JavaScript 页面时确定是否加载图片，它默认是否。</p>
                  <h4 id="save"><a href="#save" class="headerlink" title="save"></a>save</h4>
                  <p>save 参数非常有用，可以在不同的方法之间传递参数，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">on_start</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://www.example.org/'</span>, callback=<span class="keyword">self</span>.callback,</span><br><span class="line">               save=&#123;<span class="string">'page'</span>: <span class="number">1</span>&#125;)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">callback</span><span class="params">(<span class="keyword">self</span>, response)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">return</span> response.save[<span class="string">'page'</span>]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样，在 on_start() 方法中生成 Request 并传递额外的参数 page，在回调函数里可以通过 response 变量的 save 字段接收到这些参数值。</p>
                  <h4 id="cancel"><a href="#cancel" class="headerlink" title="cancel"></a>cancel</h4>
                  <p>cancel 是取消任务，如果一个任务是 ACTIVE 状态的，则需要将 force_update 设置为 True。</p>
                  <h4 id="force-update"><a href="#force-update" class="headerlink" title="force_update"></a>force_update</h4>
                  <p>即使任务处于 ACTIVE 状态，那也会强制更新状态。 以上便是 crawl() 方法的参数介绍，更加详细的描述可以参考：<a href="http://docs.pyspider.org/en/latest/apis/self.crawl/" target="_blank" rel="noopener">http://docs.pyspider.org/en/latest/apis/self.crawl/</a>。</p>
                  <h3 id="3-任务区分"><a href="#3-任务区分" class="headerlink" title="3. 任务区分"></a>3. 任务区分</h3>
                  <p>在 pyspider 判断两个任务是否是重复的是使用的是该任务对应的 URL 的 MD5 值作为任务的唯一 ID，如果 ID 相同，那么两个任务就会判定为相同，其中一个就不会爬取了。很多情况下请求的链接可能是同一个，但是 POST 的参数不同。这时可以重写 task_id() 方法，改变这个 ID 的计算方式来实现不同任务的区分，如下所示：</p>
                  <figure class="highlight gradle">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">from</span> pyspider.libs.utils <span class="keyword">import</span> md5string</span><br><span class="line"><span class="keyword">def</span> get_taskid(self, <span class="keyword">task</span>):</span><br><span class="line">    <span class="keyword">return</span> md5string(<span class="keyword">task</span>[<span class="string">'url'</span>]+json.dumps(<span class="keyword">task</span>[<span class="string">'fetch'</span>].get(<span class="string">'data'</span>, <span class="string">''</span>)))</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里重写了 get_taskid() 方法，利用 URL 和 POST 的参数来生成 ID。这样一来，即使 URL 相同，但是 POST 的参数不同，两个任务的 ID 就不同，它们就不会被识别成重复任务。</p>
                  <h3 id="4-全局配置"><a href="#4-全局配置" class="headerlink" title="4. 全局配置"></a>4. 全局配置</h3>
                  <p>pyspider 可以使用 crawl_config 来指定全局的配置，配置中的参数会和 crawl() 方法创建任务时的参数合并。如要全局配置一个 Headers，可以定义如下代码：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Handler</span><span class="params">(BaseHandler)</span>:</span></span><br><span class="line">    crawl_config = &#123;</span><br><span class="line">        <span class="string">'headers'</span>: &#123;<span class="string">'User-Agent'</span>: <span class="string">'GoogleBot'</span>,&#125;</span><br><span class="line">    &#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h3 id="5-定时爬取"><a href="#5-定时爬取" class="headerlink" title="5. 定时爬取"></a>5. 定时爬取</h3>
                  <p>我们可以通过 every 属性来设置爬取的时间间隔，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">@every(minutes=<span class="number">24</span> * <span class="number">60</span>)</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">on_start</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">for</span> url <span class="keyword">in</span> <span class="symbol">urllist:</span></span><br><span class="line">        <span class="keyword">self</span>.crawl(url, callback=<span class="keyword">self</span>.index_page)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里设置了每天执行一次爬取。 在上文中我们提到了任务的有效时间，在有效时间内爬取不会重复。所以要把有效时间设置得比重复时间更短，这样才可以实现定时爬取。 例如，下面的代码就无法做到每天爬取：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">@every(minutes=<span class="number">24</span> * <span class="number">60</span>)</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">on_start</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="string">'http://www.example.org/'</span>, callback=<span class="keyword">self</span>.index_page)</span><br><span class="line"></span><br><span class="line">@config(age=<span class="number">10</span> * <span class="number">24</span> * <span class="number">60</span> * <span class="number">60</span>)</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">index_page</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    pass</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里任务的过期时间为 10 天，而自动爬取的时间间隔为 1 天。当第二次尝试重新爬取的时候，pyspider 会监测到此任务尚未过期，便不会执行爬取，所以我们需要将 age 设置得小于定时时间。</p>
                  <h3 id="6-项目状态"><a href="#6-项目状态" class="headerlink" title="6. 项目状态"></a>6. 项目状态</h3>
                  <p>每个项目都有 6 个状态，分别是 TODO、STOP、CHECKING、DEBUG、RUNNING、PAUSE。</p>
                  <ul>
                    <li>TODO：它是项目刚刚被创建还未实现时的状态。</li>
                    <li>STOP：如果想停止某项目的抓取，可以将项目的状态设置为 STOP。</li>
                    <li>CHECKING：正在运行的项目被修改后就会变成 CHECKING 状态，项目在中途出错需要调整的时候会遇到这种情况。</li>
                    <li>DEBUG/RUNNING：这两个状态对项目的运行没有影响，状态设置为任意一个，项目都可以运行，但是可以用二者来区分项目是否已经测试通过。</li>
                    <li>PAUSE：当爬取过程中出现连续多次错误时，项目会自动设置为 PAUSE 状态，并等待一定时间后继续爬取。</li>
                  </ul>
                  <h3 id="7-抓取进度"><a href="#7-抓取进度" class="headerlink" title="7. 抓取进度"></a>7. 抓取进度</h3>
                  <p>在抓取时，可以看到抓取的进度，progress 部分会显示 4 个进度条，如图 12-27 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033821.jpg" alt=""> 图 12-27 抓取进度 progress 中的 5m、1h、1d 指的是最近 5 分、1 小时、1 天内的请求情况，all 代表所有的请求情况。 蓝色的请求代表等待被执行的任务，绿色的代表成功的任务，黄色的代表请求失败后等待重试的任务，红色的代表失败次数过多而被忽略的任务，从这里我们可以直观看到爬取的进度和请求情况。</p>
                  <h3 id="8-删除项目"><a href="#8-删除项目" class="headerlink" title="8. 删除项目"></a>8. 删除项目</h3>
                  <p>pyspider 中没有直接删除项目的选项。如要删除任务，那么将项目的状态设置为 STOP，将分组的名称设置为 delete，等待 24 小时，则项目会自动删除。</p>
                  <h3 id="9-结语"><a href="#9-结语" class="headerlink" title="9. 结语"></a>9. 结语</h3>
                  <p>以上内容便是 pyspider 的常用用法。如要了解更多，可以参考 pyspider 的官方文档：<a href="http://docs.pyspider.org/" target="_blank" rel="noopener">http://docs.pyspider.org/</a>。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-30 09:22:31" itemprop="dateCreated datePublished" datetime="2019-11-30T09:22:31+08:00">2019-11-30</time>
                </span>
                <span id="/8320.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 12.3-pyspider 用法详解" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>8.5k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>8 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8317.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8317.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 12.2-pyspider 的基本使用</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="12-2-pyspider-的基本使用"><a href="#12-2-pyspider-的基本使用" class="headerlink" title="12.2 pyspider 的基本使用"></a>12.2 pyspider 的基本使用</h1>
                  <p>本节用一个实例来讲解 pyspider 的基本用法。</p>
                  <h3 id="1-本节目标"><a href="#1-本节目标" class="headerlink" title="1. 本节目标"></a>1. 本节目标</h3>
                  <p>我们要爬取的目标是去哪儿网的旅游攻略，链接为 <a href="http://travel.qunar.com/travelbook/list.htm，我们要将所有攻略的作者、标题、出发日期、人均费用、攻略正文等保存下来，存储到" target="_blank" rel="noopener">http://travel.qunar.com/travelbook/list.htm，我们要将所有攻略的作者、标题、出发日期、人均费用、攻略正文等保存下来，存储到</a> MongoDB 中。</p>
                  <h3 id="2-准备工作"><a href="#2-准备工作" class="headerlink" title="2. 准备工作"></a>2. 准备工作</h3>
                  <p>请确保已经安装好了 pyspider 和 PhantomJS，安装好了 MongoDB 并正常运行服务，还需要安装 PyMongo 库，具体安装可以参考第 1 章的说明。</p>
                  <h3 id="3-启动-pyspider"><a href="#3-启动-pyspider" class="headerlink" title="3. 启动 pyspider"></a>3. 启动 pyspider</h3>
                  <p>执行如下命令启动 pyspider：</p>
                  <figure class="highlight ada">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">pyspider <span class="keyword">all</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行效果如图 12-2 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033547.jpg" alt=""> 图 12-2 运行结果 这样可以启动 pyspider 的所有组件，包括 PhantomJS、ResultWorker、Processer、Fetcher、Scheduler、WebUI，这些都是 pyspider 运行必备的组件。最后一行输出提示 WebUI 运行在 5000 端口上。可以打开浏览器，输入链接 <a href="http://localhost:5000，这时我们会看到页面，如图">http://localhost:5000，这时我们会看到页面，如图</a> 12-3 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033552.png" alt=""> 图 12-3 WebUI 页面 此页面便是 pyspider 的 WebUI，我们可以用它来管理项目、编写代码、在线调试、监控任务等。</p>
                  <h3 id="4-创建项目"><a href="#4-创建项目" class="headerlink" title="4. 创建项目"></a>4. 创建项目</h3>
                  <p>新建一个项目，点击右边的 Create 按钮，在弹出的浮窗里输入项目的名称和爬取的链接，再点击 Create 按钮，这样就成功创建了一个项目，如图 12-4 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033557.png" alt=""> 图 12-4 创建项目 接下来会看到 pyspider 的项目编辑和调试页面，如图 12-5 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033600.png" alt=""> 图 12-5 调试页面 左侧就是代码的调试页面，点击左侧右上角的 run 单步调试爬虫程序，在左侧下半部分可以预览当前的爬取页面。右侧是代码编辑页面，我们可以直接编辑代码和保存代码，不需要借助于 IDE。 注意右侧，pyspider 已经帮我们生成了一段代码，代码如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">from pyspider.libs.base_handler import *</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Handler</span>(<span class="title">BaseHandler</span>):</span></span><br><span class="line">    crawl_config = &#123; &#125;</span><br><span class="line"></span><br><span class="line">    @every(minutes=<span class="number">24</span> * <span class="number">60</span>)</span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">on_start</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">        <span class="keyword">self</span>.crawl(<span class="string">'http://travel.qunar.com/travelbook/list.htm'</span>, callback=<span class="keyword">self</span>.index_page)</span><br><span class="line"></span><br><span class="line">    @config(age=<span class="number">10</span> * <span class="number">24</span> * <span class="number">60</span> * <span class="number">60</span>)</span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">index_page</span><span class="params">(<span class="keyword">self</span>, response)</span></span><span class="symbol">:</span></span><br><span class="line">        <span class="keyword">for</span> each <span class="keyword">in</span> response.doc(<span class="string">'a[href^="http"]'</span>).items()<span class="symbol">:</span></span><br><span class="line">            <span class="keyword">self</span>.crawl(each.attr.href, callback=<span class="keyword">self</span>.detail_page)</span><br><span class="line"></span><br><span class="line">    @config(priority=<span class="number">2</span>)</span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">detail_page</span><span class="params">(<span class="keyword">self</span>, response)</span></span><span class="symbol">:</span></span><br><span class="line">        <span class="keyword">return</span> &#123;</span><br><span class="line">            <span class="string">"url"</span>: response.url,</span><br><span class="line">            <span class="string">"title"</span>: response.doc(<span class="string">'title'</span>).text(),&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里的 Handler 就是 pyspider 爬虫的主类，我们可以在此处定义爬取、解析、存储的逻辑。整个爬虫的功能只需要一个 Handler 即可完成。 接下来我们可以看到一个 crawl_config 属性。我们可以将本项目的所有爬取配置统一定义到这里，如定义 Headers、设置代理等，配置之后全局生效。 然后，on_start() 方法是爬取入口，初始的爬取请求会在这里产生，该方法通过调用 crawl() 方法即可新建一个爬取请求，第一个参数是爬取的 URL，这里自动替换成我们所定义的 URL。crawl() 方法还有一个参数 callback，它指定了这个页面爬取成功后用哪个方法进行解析，代码中指定为 index_page() 方法，即如果这个 URL 对应的页面爬取成功了，那 Response 将交给 index_page() 方法解析。 index_page() 方法恰好接收这个 Response 参数，Response 对接了 pyquery。我们直接调用 doc() 方法传入相应的 CSS 选择器，就可以像 pyquery 一样解析此页面，代码中默认是 a[href^=”http”]，也就是说该方法解析了页面的所有链接，然后将链接遍历，再次调用了 crawl() 方法生成了新的爬取请求，同时再指定了 callback 为 detail_page，意思是说这些页面爬取成功了就调用 detail_page() 方法解析。这里，index_page() 实现了两个功能，一是将爬取的结果进行解析，二是生成新的爬取请求。 detail_page() 同样接收 Response 作为参数。detail_page() 抓取的就是详情页的信息，就不会生成新的请求，只对 Response 对象做解析，解析之后将结果以字典的形式返回。当然我们也可以进行后续处理，如将结果保存到数据库。 接下来，我们改写一下代码来实现攻略的爬取吧。</p>
                  <h3 id="5-爬取首页"><a href="#5-爬取首页" class="headerlink" title="5. 爬取首页"></a>5. 爬取首页</h3>
                  <p>点击左栏右上角的 run 按钮，即可看到页面下方 follows 便会出现一个标注，其中包含数字 1，这代表有新的爬取请求产生，如图 12-6 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033606.jpg" alt=""> 图 12-6 操作示例 左栏左上角会出现当前 run 的配置文件，这里有一个 callback 为 on_start，这说明点击 run 之后实际是执行了 on_start() 方法。在 on_start() 方法中，我们利用 crawl() 方法生成一个爬取请求，那下方 follows 部分的数字 1 就代表了这一个爬取请求。 点击下方的 follows 按钮，即可看到生成的爬取请求的链接。每个链接的右侧还有一个箭头按钮，如图 12-7 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033611.jpg" alt=""> 图 12-7 操作示例 点击该箭头，我们就可以对此链接进行爬取，也就是爬取攻略的首页内容，如图 12-8 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033616.jpg" alt=""> 图 12-8 爬取结果 上方的 callback 已经变成了 index_page，这就代表当前运行了 index_page() 方法。index_page() 接收到的 response 参数就是刚才生成的第一个爬取请求的 Response 对象。index_page() 方法通过调用 doc() 方法，传入提取所有 a 节点的 CSS 选择器，然后获取 a 节点的属性 href，这样实际上就是获取了第一个爬取页面中的所有链接。然后在 index_page() 方法里遍历了所有链接，同时调用 crawl() 方法，就把这一个个的链接构造成新的爬取请求了。所以最下方 follows 按钮部分有 217 的数字标记，这代表新生成了 217 个爬取请求，同时这些请求的 URL 都呈现在当前页面了。 再点击下方的 web 按钮，即可预览当前爬取结果的页面，如图 12-9 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033620.jpg" alt=""> 图 12-9 预览页面 当前看到的页面结果和浏览器看到的几乎是完全一致的，在这里我们可以方便地查看页面请求的结果。 点击 html 按钮即可查看当前页面的源代码，如图 12-10 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033626.jpg" alt=""> 图 12-10 页面源码 如果需要分析代码的结构，我们可以直接参考页面源码。 我们刚才在 index_page() 方法中提取了所有的链接并生成了新的爬取请求。但是很明显要爬取的肯定不是所有链接，只需要攻略详情的页面链接就够了，所以我们要修改一下当前 index_page() 里提取链接时的 CSS 选择器。 接下来需要另外一个工具。首先切换到 Web 页面，找到攻略的标题，点击下方的 enable css selector helper，点击标题。这时候我们看到标题外多了一个红框，上方出现了一个 CSS 选择器，这就是当前标题对应的 CSS 选择器，如图 12-11 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033631.jpg" alt=""> 图 12-11 CSS 工具 在右侧代码选中要更改的区域，点击左栏的右箭头，此时在上方出现的标题的 CSS 选择器就会被替换到右侧代码中，如图 12-12 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033637.png" alt=""> 图 12-12 操作结果 这样就成功完成了 CSS 选择器的替换，非常便捷。 重新点击左栏右上角的 run 按钮，即可重新执行 index_page() 方法。此时的 follows 就变成了 10 个，也就是说现在我们提取的只有当前页面的 10 个攻略，如图 12-13 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033647.jpg" alt=""> 图 12-13 运行结果 我们现在抓取的只是第一页的内容，还需要抓取后续页面，所以还需要一个爬取链接，即爬取下一页的攻略列表页面。我们再利用 crawl() 方法添加下一页的爬取请求，在 index_page() 方法里面添加如下代码，然后点击 save 保存：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">next</span> = response.doc(<span class="string">'.next'</span>).attr.href</span><br><span class="line"><span class="keyword">self</span>.crawl(<span class="keyword">next</span>, callback=<span class="keyword">self</span>.index_page)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>利用 CSS 选择器选中下一页的链接，获取它的 href 属性，也就获取了页面的 URL。然后将该 URL 传给 crawl() 方法，同时指定回调函数，注意这里回调函数仍然指定为 index_page() 方法，因为下一页的结构与此页相同。 重新点击 run 按钮，这时就可以看到 11 个爬取请求。follows 按钮上会显示 11，这就代表我们成功添加了下一页的爬取请求，如图 12-14 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033657.jpg" alt=""> 图 12-14 运行结果 现在，索引列表页的解析过程我们就完成了。</p>
                  <h3 id="6-爬取详情页"><a href="#6-爬取详情页" class="headerlink" title="6. 爬取详情页"></a>6. 爬取详情页</h3>
                  <p>任意选取一个详情页进入，点击前 10 个爬取请求中的任意一个的右箭头，执行详情页的爬取，如图 12-15 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033702.jpg" alt=""> 图 12-15 运行结果 切换到 Web 页面预览效果，页面下拉之后，头图正文中的一些图片一直显示加载中，如图 12-16 和图 12-17 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033707.jpg" alt=""> 图 12-16 预览结果 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033712.jpg" alt=""> 图 12-17 预览结果 查看源代码，我们没有看到 img 节点，如图 12-18 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033717.jpg" alt=""> 图 12-18 源代码 出现此现象的原因是 pyspider 默认发送 HTTP 请求，请求的 HTML 文档本身就不包含 img 节点。但是在浏览器中我们看到了图片，这是因为这张图片是后期经过 JavaScript 出现的。那么，我们该如何获取呢？ 幸运的是，pyspider 内部对接了 PhantomJS，那么我们只需要修改一个参数即可。 我们将 index_page() 中生成抓取详情页的请求方法添加一个参数 fetch_type，改写的 index_page() 变为如下内容：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">index_page</span><span class="params">(<span class="keyword">self</span>, response)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">for</span> each <span class="keyword">in</span> response.doc(<span class="string">'li&gt; .tit &gt; a'</span>).items()<span class="symbol">:</span></span><br><span class="line">        <span class="keyword">self</span>.crawl(each.attr.href, callback=<span class="keyword">self</span>.detail_page, fetch_type=<span class="string">'js'</span>)</span><br><span class="line">    <span class="keyword">next</span> = response.doc(<span class="string">'.next'</span>).attr.href</span><br><span class="line">    <span class="keyword">self</span>.crawl(<span class="keyword">next</span>, callback=<span class="keyword">self</span>.index_page)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>接下来，我们来试试它的抓取效果。 点击左栏上方的左箭头返回，重新调用 index_page() 方法生成新的爬取详情页的 Request，如图 12-19 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033723.jpg" alt=""> 图 12-19 爬取详情 再点击新生成的详情页 Request 的爬取按钮，这时我们便可以看到页面变成了这样子，如图 12-20 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033727.jpg" alt=""> 图 12-20 运行结果 图片被成功渲染出来，这就是启用了 PhantomJS 渲染后的结果。只需要添加一个 fetch_type 参数即可，这非常方便。 最后就是将详情页中需要的信息提取出来，提取过程不再赘述。最终 detail_page() 方法改写如下所示：</p>
                  <figure class="highlight xquery">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def detail_page(self, response):</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="string">'url'</span>: response.url,</span><br><span class="line">        <span class="string">'title'</span>: response<span class="built_in">.doc</span>(<span class="string">'#booktitle'</span>).<span class="type">text</span>(),</span><br><span class="line">        <span class="string">'date'</span>: response<span class="built_in">.doc</span>(<span class="string">'.when .data'</span>).<span class="type">text</span>(),</span><br><span class="line">        <span class="string">'day'</span>: response<span class="built_in">.doc</span>(<span class="string">'.howlong .data'</span>).<span class="type">text</span>(),</span><br><span class="line">        <span class="string">'who'</span>: response<span class="built_in">.doc</span>(<span class="string">'.who .data'</span>).<span class="type">text</span>(),</span><br><span class="line">        <span class="string">'text'</span>: response<span class="built_in">.doc</span>(<span class="string">'#b_panel_schedule'</span>).<span class="type">text</span>(),</span><br><span class="line">        <span class="string">'image'</span>: response<span class="built_in">.doc</span>(<span class="string">'.cover_img'</span>).attr.src</span><br><span class="line">    &#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们分别提取了页面的链接、标题、出行日期、出行天数、人物、攻略正文、头图信息，将这些信息构造成一个字典。 重新运行，即可发现输出结果如图 12-21 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033733.png" alt=""> 图 12-21 输出结果 左栏中输出了最终构造的字典信息，这就是一篇攻略的抓取结果。</p>
                  <h3 id="7-启动爬虫"><a href="#7-启动爬虫" class="headerlink" title="7. 启动爬虫"></a>7. 启动爬虫</h3>
                  <p>返回爬虫的主页面，将爬虫的 status 设置成 DEBUG 或 RUNNING，点击右侧的 Run 按钮即可开始爬取，如图 12-22 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033737.jpg" alt=""> 图 12-22 启动爬虫 在最左侧我们可以定义项目的分组，以方便管理。rate/burst 代表当前的爬取速率，rate 代表 1 秒发出多少个请求，burst 相当于流量控制中的令牌桶算法的令牌数，rate 和 burst 设置的越大，爬取速率越快，当然速率需要考虑本机性能和爬取过快被封的问题。process 中的 5m、1h、1d 指的是最近 5 分、1 小时、1 天内的请求情况，all 代表所有的请求情况。请求由不同颜色表示，蓝色的代表等待被执行的请求，绿色的代表成功的请求，黄色的代表请求失败后等待重试的请求，红色的代表失败次数过多而被忽略的请求，这样可以直观知道爬取的进度和请求情况，如图 12-23 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033743.jpg" alt=""> 图 12-23 爬取情况 点击 Active Tasks，即可查看最近请求的详细状况，如图 12-24 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033748.png" alt=""> 图 12-24 最近请求 点击 Results，即可查看所有的爬取结果，如图 12-25 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033754.png" alt=""> 图 12-25 爬取结果 点击右上角的按钮，即可获取数据的 JSON、CSV 格式。</p>
                  <h3 id="8-本节代码"><a href="#8-本节代码" class="headerlink" title="8. 本节代码"></a>8. 本节代码</h3>
                  <p>本节代码地址为：<a href="https://github.com/Python3WebSpider/Qunar" target="_blank" rel="noopener">https://github.com/Python3WebSpider/Qunar</a>。</p>
                  <h3 id="9-结语"><a href="#9-结语" class="headerlink" title="9. 结语"></a>9. 结语</h3>
                  <p>本节介绍了 pyspider 的基本用法，接下来我们会更加深入了解它的详细使用。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-30 09:20:54" itemprop="dateCreated datePublished" datetime="2019-11-30T09:20:54+08:00">2019-11-30</time>
                </span>
                <span id="/8317.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 12.2-pyspider 的基本使用" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>5.6k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>5 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8309.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8309.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 12.1-pyspider 框架介绍</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="12-1-pyspider-框架介绍"><a href="#12-1-pyspider-框架介绍" class="headerlink" title="12.1 pyspider 框架介绍"></a>12.1 pyspider 框架介绍</h1>
                  <p>pyspider 是由国人 binux 编写的强大的网络爬虫系统，其 GitHub 地址为 <a href="https://github.com/binux/pyspider，官方文档地址为" target="_blank" rel="noopener">https://github.com/binux/pyspider，官方文档地址为</a> <a href="http://docs.pyspider.org/" target="_blank" rel="noopener">http://docs.pyspider.org/</a>。 pyspider 带有强大的 WebUI、脚本编辑器、任务监控器、项目管理器以及结果处理器，它支持多种数据库后端、多种消息队列、JavaScript 渲染页面的爬取，使用起来非常方便。</p>
                  <h3 id="1-pyspider-基本功能"><a href="#1-pyspider-基本功能" class="headerlink" title="1. pyspider 基本功能"></a>1. pyspider 基本功能</h3>
                  <p>我们总结了一下，PySpider 的功能有如下几点。</p>
                  <ul>
                    <li>提供方便易用的 WebUI 系统，可以可视化地编写和调试爬虫。</li>
                    <li>提供爬取进度监控、爬取结果查看、爬虫项目管理等功能。</li>
                    <li>支持多种后端数据库，如 MySQL、MongoDB、Redis、SQLite、Elasticsearch、PostgreSQL。</li>
                    <li>支持多种消息队列，如 RabbitMQ、Beanstalk、Redis、Kombu。</li>
                    <li>提供优先级控制、失败重试、定时抓取等功能。</li>
                    <li>对接了 PhantomJS，可以抓取 JavaScript 渲染的页面。</li>
                    <li>支持单机和分布式部署，支持 Docker 部署。</li>
                  </ul>
                  <p>如果想要快速方便地实现一个页面的抓取，使用 pyspider 不失为一个好的选择。</p>
                  <h3 id="2-与-Scrapy-的比较"><a href="#2-与-Scrapy-的比较" class="headerlink" title="2. 与 Scrapy 的比较"></a>2. 与 Scrapy 的比较</h3>
                  <p>后面会介绍另外一个爬虫框架 Scrapy，我们学习完 Scrapy 之后会更容易理解此部分内容。我们先了解一下 pyspider 与 Scrapy 的区别。</p>
                  <ul>
                    <li>pyspider 提供了 WebUI，爬虫的编写、调试都是在 WebUI 中进行的，而 Scrapy 原生是不具备这个功能的，采用的是代码和命令行操作，但可以通过对接 Portia 实现可视化配置。</li>
                    <li>pyspider 调试非常方便，WebUI 操作便捷直观，在 Scrapy 中则是使用 parse 命令进行调试，论方便程度不及 pyspider。</li>
                    <li>pyspider 支持 PhantomJS 来进行 JavaScript 渲染页面的采集，在 Scrapy 中可以对接 ScrapySplash 组件，需要额外配置。</li>
                    <li>PySpide r 中内置了 PyQuery 作为选择器，在 Scrapy 中对接了 XPath、CSS 选择器和正则匹配。</li>
                    <li>pyspider 的可扩展程度不足，可配制化程度不高，在 Scrapy 中可以通过对接 Middleware、Pipeline、Extension 等组件实现非常强大的功能，模块之间的耦合程度低，可扩展程度极高。</li>
                  </ul>
                  <p>如果要快速实现一个页面的抓取，推荐使用 pyspider，开发更加便捷，如快速抓取某个普通新闻网站的新闻内容。如果要应对反爬程度很强、超大规模的抓取，推荐使用 Scrapy，如抓取封 IP、封账号、高频验证的网站的大规模数据采集。</p>
                  <h3 id="3-pyspider-的架构"><a href="#3-pyspider-的架构" class="headerlink" title="3. pyspider 的架构"></a>3. pyspider 的架构</h3>
                  <p>pyspider 的架构主要分为 Scheduler（调度器）、Fetcher（抓取器）、Processer（处理器）三个部分，整个爬取过程受到 Monitor（监控器）的监控，抓取的结果被 Result Worker（结果处理器）处理，如图 12-1 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-033539.jpg" alt=""> 图 12-1 pyspider 架构图 Scheduler 发起任务调度，Fetcher 负责抓取网页内容，Processer 负责解析网页内容，然后将新生成的 Request 发给 Scheduler 进行调度，将生成的提取结果输出保存。 pyspider 的任务执行流程的逻辑很清晰，具体过程如下所示。</p>
                  <ul>
                    <li>每个 pyspider 的项目对应一个 Python 脚本，该脚本中定义了一个 Handler 类，它有一个 on_start() 方法。爬取首先调用 on_start() 方法生成最初的抓取任务，然后发送给 Scheduler 进行调度。</li>
                    <li>Scheduler 将抓取任务分发给 Fetcher 进行抓取，Fetcher 执行并得到响应，随后将响应发送给 Processer。</li>
                    <li>Processer 处理响应并提取出新的 URL 生成新的抓取任务，然后通过消息队列的方式通知 Schduler 当前抓取任务执行情况，并将新生成的抓取任务发送给 Scheduler。如果生成了新的提取结果，则将其发送到结果队列等待 Result Worker 处理。</li>
                    <li>Scheduler 接收到新的抓取任务，然后查询数据库，判断其如果是新的抓取任务或者是需要重试的任务就继续进行调度，然后将其发送回 Fetcher 进行抓取。</li>
                    <li>不断重复以上工作，直到所有的任务都执行完毕，抓取结束。</li>
                    <li>抓取结束后，程序会回调 on_finished() 方法，这里可以定义后处理过程。</li>
                  </ul>
                  <h3 id="4-结语"><a href="#4-结语" class="headerlink" title="4. 结语"></a>4. 结语</h3>
                  <p>本节我们主要了解了 pyspider 的基本功能和架构。接下来我们会用实例来体验一下 pyspider 的抓取操作，然后总结它的各种用法。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-29 09:43:40" itemprop="dateCreated datePublished" datetime="2019-11-29T09:43:40+08:00">2019-11-29</time>
                </span>
                <span id="/8309.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 12.1-pyspider 框架介绍" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>1.9k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>2 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8306.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8306.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 11.6-Appium+mitmdump 爬取京东商品</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="11-6-Appium-mitmdump-爬取京东商品"><a href="#11-6-Appium-mitmdump-爬取京东商品" class="headerlink" title="11.6 Appium+mitmdump 爬取京东商品"></a>11.6 Appium+mitmdump 爬取京东商品</h1>
                  <p>在前文中，我们曾经用 Charles 分析过京东商品的评论数据，但是可以发现其参数相当复杂，Form 表单有很多加密参数。如果我们只用 Charles 探测到这个接口链接和参数，还是无法直接构造请求的参数，构造的过程涉及一些加密算法，也就无法直接还原抓取过程。</p>
                  <p>我们了解了 mitmproxy 的用法，利用它的 mitmdump 组件，可以直接对接 Python 脚本对抓取的数据包进行处理，用 Python 脚本对请求和响应直接进行处理。这样我们可以绕过请求的参数构造过程，直接监听响应进行处理即可。但是这个过程并不是自动化的，抓取 App 的时候实际是人工模拟了这个拖动过程。如果这个操作可以用程序来实现就更好了。</p>
                  <p>我们又了解了 Appium 的用法，它可以指定自动化脚本模拟实现 App 的一系列动作，如点击、拖动等，也可以提取 App 中呈现的信息。经过上节爬取微信朋友圈的实例，我们知道解析过程比较烦琐，而且速度要加以限制。如果内容没有显示出来解析就会失败，而且还会导致重复提取的问题。更重要的是，它只可以获取在 App 中看到的信息，无法直接提取接口获取的真实数据，而接口的数据往往是最易提取且信息量最全的。</p>
                  <p>综合以上几点，我们就可以确定出一个解决方案了。如果我们用 mitmdump 去监听接口数据，用 Appium 去模拟 App 的操作，就可以绕过复杂的接口参数又可以实现自动化抓取了！这种方式应是抓取 App 数据的最佳方式。某些特殊情况除外，如微信朋友圈数据又经过了一次加密无法解析，而只能用 Appium 提取。但是对于大多数 App 来说，此种方法是奏效的。本节我们用一个实例感受一下这种抓取方式的便捷之处。</p>
                  <h3 id="1-本节目标"><a href="#1-本节目标" class="headerlink" title="1. 本节目标"></a>1. 本节目标</h3>
                  <p>以抓取京东 App 的商品信息和评论为例，实现 Appium 和 mitmdump 二者结合的抓取。抓取的数据分为两部分：一部分是商品信息，我们需要获取商品的 ID、名称和图片，将它们组成一条商品数据；另一部分是商品的评论信息，我们将评论人的昵称、评论正文、评论日期、发表图片都提取，然后加入商品 ID 字段，将它们组成一条评论数据。最后数据保存到 MongoDB 数据库。</p>
                  <h3 id="2-准备工作"><a href="#2-准备工作" class="headerlink" title="2. 准备工作"></a>2. 准备工作</h3>
                  <p>请确保 PC 已经安装好 Charles、mitmdump、Appium、Android 开发环境，以及 Python 版本的 Appium API。Android 手机安装好京东 App。另外，安装好 MongoDB 并运行其服务，安装 PyMongo 库。具体的配置过程可以参考第 1 章。</p>
                  <h3 id="3-Charles-抓包分析"><a href="#3-Charles-抓包分析" class="headerlink" title="3. Charles 抓包分析"></a>3. Charles 抓包分析</h3>
                  <p>首先，我们将手机代理设置到 Charles 上，用 Charles 抓包分析获取商品详情和商品评论的接口。</p>
                  <p>获取商品详情的接口，这里提取到的接口是来自 cdnware.m.jd.com 的链接，返回结果是一个 JSON 字符串，里面包含了商品的 ID 和商品名称，如图 11-47 和图 11-48 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033508.jpg" alt=""></p>
                  <p>图 11-47 请求概览</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033512.jpg" alt=""></p>
                  <p>图 11-48 响应结果</p>
                  <p>再获取商品评论的接口，这个过程在前文已提到，在此不再赘述。这个接口来自 api.m.jd.com，返回结果也是 JSON 字符串，里面包含了商品的数条评论信息。</p>
                  <p>之后我们可以用 mitmdump 对接一个 Python 脚本来实现数据的抓取。</p>
                  <h3 id="4-mitmdump-抓取"><a href="#4-mitmdump-抓取" class="headerlink" title="4. mitmdump 抓取"></a>4. mitmdump 抓取</h3>
                  <p>新建一个脚本文件，然后实现这个脚本以提取这两个接口的数据。首先提取商品的信息，代码如下所示：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def response(flow):</span><br><span class="line">    url = <span class="string">'cdnware.m.jd.com'</span></span><br><span class="line">    <span class="keyword">if</span> url <span class="keyword">in</span> flow.request.url:</span><br><span class="line">        text = flow.response.text</span><br><span class="line">        data = json.loads(text)</span><br><span class="line">        <span class="keyword">if</span> data.<span class="builtin-name">get</span>(<span class="string">'wareInfo'</span>) <span class="keyword">and</span> data.<span class="builtin-name">get</span>(<span class="string">'wareInfo'</span>).<span class="builtin-name">get</span>(<span class="string">'basicInfo'</span>):</span><br><span class="line">            <span class="builtin-name">info</span> = data.<span class="builtin-name">get</span>(<span class="string">'wareInfo'</span>).<span class="builtin-name">get</span>(<span class="string">'basicInfo'</span>)</span><br><span class="line">            id = info.<span class="builtin-name">get</span>(<span class="string">'wareId'</span>)</span><br><span class="line">            name = info.<span class="builtin-name">get</span>(<span class="string">'name'</span>)</span><br><span class="line">            images = info.<span class="builtin-name">get</span>(<span class="string">'wareImage'</span>)</span><br><span class="line">            <span class="builtin-name">print</span>(id, name, images)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里声明了接口的部分链接内容，然后与请求的 URL 作比较。如果该链接出现在当前的 URL 中，那就证明当前的响应就是商品详情的响应，然后提取对应的 JSON 信息即可。在这里我们将商品的 ID、名称和图片提取出来，这就是一条商品数据。</p>
                  <p>再提取评论的数据，代码实现如下所示：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment"># 提取评论数据</span></span><br><span class="line">url = <span class="string">'api.m.jd.com/client.action'</span></span><br><span class="line"><span class="keyword">if</span> url <span class="keyword">in</span> flow.request.url:</span><br><span class="line">    pattern = re.compile(<span class="string">'sku".*?"(d+)"'</span>)</span><br><span class="line">    # Request 请求参数中包含商品 ID</span><br><span class="line">    body = unquote(flow.request.text)</span><br><span class="line">    # 提取商品 ID</span><br><span class="line">    id = re.search(pattern, body).group(1) <span class="keyword">if</span> re.search(pattern, body) <span class="keyword">else</span> None</span><br><span class="line">    # 提取 Response Body</span><br><span class="line">    text = flow.response.text</span><br><span class="line">    data = json.loads(text)</span><br><span class="line">    comments = data.<span class="builtin-name">get</span>(<span class="string">'commentInfoList'</span>) <span class="keyword">or</span> []</span><br><span class="line">    # 提取评论数据</span><br><span class="line">    <span class="keyword">for</span> comment <span class="keyword">in</span> comments:</span><br><span class="line">        <span class="keyword">if</span> comment.<span class="builtin-name">get</span>(<span class="string">'commentInfo'</span>) <span class="keyword">and</span> comment.<span class="builtin-name">get</span>(<span class="string">'commentInfo'</span>).<span class="builtin-name">get</span>(<span class="string">'commentData'</span>):</span><br><span class="line">            <span class="builtin-name">info</span> = comment.<span class="builtin-name">get</span>(<span class="string">'commentInfo'</span>)</span><br><span class="line">            text = info.<span class="builtin-name">get</span>(<span class="string">'commentData'</span>)</span><br><span class="line">            date = info.<span class="builtin-name">get</span>(<span class="string">'commentDate'</span>)</span><br><span class="line">            nickname = info.<span class="builtin-name">get</span>(<span class="string">'userNickName'</span>)</span><br><span class="line">            pictures = info.<span class="builtin-name">get</span>(<span class="string">'pictureInfoList'</span>)</span><br><span class="line">            <span class="builtin-name">print</span>(id, nickname, text, date, pictures)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里指定了接口的部分链接内容，以判断当前请求的 URL 是不是获取评论的 URL。如果满足条件，那么就提取商品的 ID 和评论信息。</p>
                  <p>商品的 ID 实际上隐藏在请求中，我们需要提取请求的表单内容来提取商品的 ID，这里直接用了正则表达式。</p>
                  <p>商品的评论信息在响应中，我们像刚才一样提取了响应的内容，然后对 JSON 进行解析，最后提取出商品评论人的昵称、评论正文、评论日期和图片信息。这些信息和商品的 ID 组合起来，形成一条评论数据。</p>
                  <p>最后用 MongoDB 将两部分数据分开保存到两个 Collection，在此不再赘述。</p>
                  <p>运行此脚本，命令如下所示：</p>
                  <figure class="highlight applescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">mitmdump -s <span class="keyword">script</span>.py</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>手机的代理设置到 mitmdump 上。我们在京东 App 中打开某个商品，下拉商品评论部分，即可看到控制台输出两部分的抓取结果，结果成功保存到 MongoDB 数据库，如图 11-49 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033523.jpg" alt=""></p>
                  <p>图 11-49 保存结果</p>
                  <p>如果我们手动操作京东 App 就可以做到京东商品评论的抓取了，下一步要做的就是实现自动滚动刷新。</p>
                  <h3 id="5-Appium-自动化"><a href="#5-Appium-自动化" class="headerlink" title="5. Appium 自动化"></a>5. Appium 自动化</h3>
                  <p>将 Appium 对接到手机上，用 Appium 驱动 App 完成一系列动作。进入 App 后，我们需要做的操作有点击搜索框、输入搜索的商品名称、点击进入商品详情、进入评论页面、自动滚动刷新，基本的操作逻辑和爬取微信朋友圈的相同。</p>
                  <p>京东 App 的 Desired Capabilities 配置如下所示：</p>
                  <figure class="highlight sml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;</span><br><span class="line">    <span class="symbol">'platformName'</span>: <span class="symbol">'Android'</span>,</span><br><span class="line">    <span class="symbol">'deviceName'</span>: <span class="symbol">'MI_NOTE_Pro'</span>,</span><br><span class="line">    <span class="symbol">'appPackage'</span>: <span class="symbol">'com</span>.jingdong.app.mall',</span><br><span class="line">    <span class="symbol">'appActivity'</span>: <span class="symbol">'main</span>.<span class="type">MainActivity'</span></span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>首先用 Appium 内置的驱动打开京东 App，如图 11-50 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033528.jpg" alt=""></p>
                  <p>图 11-50 调试界面</p>
                  <p>这里进行一系动作操作并录制下来，找到各个页面的组件的 ID 并做好记录，最后再改写成完整的代码。参考代码实现如下所示：</p>
                  <figure class="highlight haskell">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="title">from</span> appium <span class="keyword">import</span> webdriver</span><br><span class="line"><span class="title">from</span> selenium.webdriver.common.by <span class="keyword">import</span> By</span><br><span class="line"><span class="title">from</span> selenium.webdriver.support.ui <span class="keyword">import</span> WebDriverWait</span><br><span class="line"><span class="title">from</span> selenium.webdriver.support <span class="keyword">import</span> expected_conditions <span class="keyword">as</span> EC</span><br><span class="line"><span class="title">from</span> time <span class="keyword">import</span> sleep</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="type">Action</span>():</span></span><br><span class="line"><span class="class">    def __init__(<span class="title">self</span>):</span></span><br><span class="line"><span class="class">        # 驱动配置</span></span><br><span class="line"><span class="class">        self.desired_caps = &#123;</span></span><br><span class="line"><span class="class">            'platformName': <span class="type">PLATFORM</span>,</span></span><br><span class="line"><span class="class">            'deviceName': <span class="type">DEVICE_NAME</span>,</span></span><br><span class="line"><span class="class">            'appPackage': 'com.jingdong.app.mall',</span></span><br><span class="line"><span class="class">            'appActivity': 'main.<span class="type">MainActivity'</span></span></span><br><span class="line"><span class="class">        &#125;</span></span><br><span class="line"><span class="class">        self.driver = webdriver.<span class="type">Remote</span>(<span class="type">DRIVER_SERVER</span>, <span class="title">self</span>.<span class="title">desired_caps</span>)</span></span><br><span class="line"><span class="class">        self.wait = <span class="type">WebDriverWait</span>(<span class="title">self</span>.<span class="title">driver</span>, <span class="type">TIMEOUT</span>)</span></span><br><span class="line"><span class="class"></span></span><br><span class="line"><span class="class">    def comments(<span class="title">self</span>):</span></span><br><span class="line"><span class="class">        # 点击进入搜索页面</span></span><br><span class="line"><span class="class">        search = self.wait.until(<span class="type">EC</span>.<span class="title">presence_of_element_located</span>((<span class="type">By</span>.<span class="type">ID</span>, '<span class="title">com</span>.<span class="title">jingdong</span>.<span class="title">app</span>.<span class="title">mall</span>:<span class="title">id</span>/<span class="title">mp'</span>)))</span></span><br><span class="line"><span class="class">        search.click()</span></span><br><span class="line"><span class="class">        # 输入搜索文本</span></span><br><span class="line"><span class="class">        box = self.wait.until(<span class="type">EC</span>.<span class="title">presence_of_element_located</span>((<span class="type">By</span>.<span class="type">ID</span>, '<span class="title">com</span>.<span class="title">jd</span>.<span class="title">lib</span>.<span class="title">search</span>:<span class="title">id</span>/<span class="title">search_box_layout'</span>)))</span></span><br><span class="line"><span class="class">        box.set_text(<span class="type">KEYWORD</span>)</span></span><br><span class="line"><span class="class">        # 点击搜索按钮</span></span><br><span class="line"><span class="class">        button = self.wait.until(<span class="type">EC</span>.<span class="title">presence_of_element_located</span>((<span class="type">By</span>.<span class="type">ID</span>, '<span class="title">com</span>.<span class="title">jd</span>.<span class="title">lib</span>.<span class="title">search</span>:<span class="title">id</span>/<span class="title">search_btn'</span>)))</span></span><br><span class="line"><span class="class">        button.click()</span></span><br><span class="line"><span class="class">        # 点击进入商品详情</span></span><br><span class="line"><span class="class">        view = self.wait.until(<span class="type">EC</span>.<span class="title">presence_of_element_located</span>((<span class="type">By</span>.<span class="type">ID</span>, '<span class="title">com</span>.<span class="title">jd</span>.<span class="title">lib</span>.<span class="title">search</span>:<span class="title">id</span>/<span class="title">product_list_item'</span>)))</span></span><br><span class="line"><span class="class">        view.click()</span></span><br><span class="line"><span class="class">        # 进入评论详情</span></span><br><span class="line"><span class="class">        tab = self.wait.until(<span class="type">EC</span>.<span class="title">presence_of_element_located</span>((<span class="type">By</span>.<span class="type">ID</span>, '<span class="title">com</span>.<span class="title">jd</span>.<span class="title">lib</span>.<span class="title">productdetail</span>:<span class="title">id</span>/<span class="title">pd_tab3'</span>)))</span></span><br><span class="line"><span class="class">        tab.click()</span></span><br><span class="line"><span class="class"></span></span><br><span class="line"><span class="class">    def scroll(<span class="title">self</span>):</span></span><br><span class="line"><span class="class">        while <span class="type">True</span>:</span></span><br><span class="line"><span class="class">            # 模拟拖动</span></span><br><span class="line"><span class="class">            self.driver.swipe(<span class="type">FLICK_START_X</span>, <span class="type">FLICK_START_Y</span> + <span class="type">FLICK_DISTANCE</span>, <span class="type">FLICK_START_X</span>, <span class="type">FLICK_START_Y</span>)</span></span><br><span class="line"><span class="class">            sleep(<span class="type">SCROLL_SLEEP_TIME</span>)</span></span><br><span class="line"><span class="class"></span></span><br><span class="line"><span class="class">    def main(<span class="title">self</span>):</span></span><br><span class="line"><span class="class">        self.comments()</span></span><br><span class="line"><span class="class">        self.scroll()</span></span><br><span class="line"><span class="class"></span></span><br><span class="line"><span class="class">if __name__ == '__main__':</span></span><br><span class="line"><span class="class">    action = <span class="type">Action</span>()</span></span><br><span class="line"><span class="class">    action.main()</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>代码实现比较简单，逻辑与上一节微信朋友圈的抓取类似。注意，由于 App 版本更新的原因，交互流程和元素 ID 可能有更改，这里的代码仅做参考。</p>
                  <p>下拉过程已经省去了用 Appium 提取数据的过程，因为这个过程我们已经用 mitmdump 帮助实现了。</p>
                  <p>代码运行之后便会启动京东 App，进入商品的详情页，然后进入评论页再无限滚动，这样就代替了人工操作。Appium 实现模拟滚动，mitmdump 进行抓取，这样 App 的数据就会保存到数据库中。</p>
                  <h3 id="6-本节代码"><a href="#6-本节代码" class="headerlink" title="6. 本节代码"></a>6. 本节代码</h3>
                  <p>本节代码地址：<a href="https://github.com/Python3WebSpider/MitmAppiumJD" target="_blank" rel="noopener"></a><a href="https://github.com/Python3WebSpider/MitmAppiumJD" target="_blank" rel="noopener">https://github.com/Python3WebSpider/MitmAppiumJD</a>。</p>
                  <h3 id="7-结语"><a href="#7-结语" class="headerlink" title="7. 结语"></a>7. 结语</h3>
                  <p>以上内容便是 Appium 和 mitmdump 抓取京东 App 数据的过程。有了两者的配合，我们既可以做到实时数据处理，又可以实现自动化爬取，这样就可以完成绝大多数 App 的爬取了。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-29 09:41:33" itemprop="dateCreated datePublished" datetime="2019-11-29T09:41:33+08:00">2019-11-29</time>
                </span>
                <span id="/8306.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 11.6-Appium+mitmdump 爬取京东商品" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>5.1k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>5 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8293.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8293.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 11.5-Appium 爬取微信朋友圈</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="11-5-Appium-爬取微信朋友圈"><a href="#11-5-Appium-爬取微信朋友圈" class="headerlink" title="11.5 Appium 爬取微信朋友圈"></a>11.5 Appium 爬取微信朋友圈</h1>
                  <p>接下来，我们将实现微信朋友圈的爬取。</p>
                  <p>如果直接用 Charles 或 mitmproxy 来监听微信朋友圈的接口数据，这是无法实现爬取的，因为数据都是被加密的。而 Appium 不同，Appium 作为一个自动化测试工具可以直接模拟 App 的操作并可以获取当前所见的内容。所以只要 App 显示了内容，我们就可以用 Appium 抓取下来。</p>
                  <h3 id="1-本节目标"><a href="#1-本节目标" class="headerlink" title="1. 本节目标"></a>1. 本节目标</h3>
                  <p>本节我们以 Android 平台为例，实现抓取微信朋友圈的动态信息。动态信息包括好友昵称、正文、发布日期。其中发布日期还需要进行转换，如日期显示为 1 小时前，则时间转换为今天，最后动态信息保存到 MongoDB。</p>
                  <h3 id="2-准备工作"><a href="#2-准备工作" class="headerlink" title="2. 准备工作"></a>2. 准备工作</h3>
                  <p>请确保 PC 已经安装好 Appium、Android 开发环境和 Python 版本的 Appium API。Android 手机安装好微信 App、PyMongo 库，安装 MongoDB 并运行其服务，安装方法可以参考第 1 章。</p>
                  <h3 id="3-初始化"><a href="#3-初始化" class="headerlink" title="3. 初始化"></a>3. 初始化</h3>
                  <p>首先新建一个 Moments 类，进行一些初始化配置，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">PLATFORM = <span class="string">'Android'</span></span><br><span class="line">DEVICE_NAME = <span class="string">'MI_NOTE_Pro'</span></span><br><span class="line">APP_PACKAGE = <span class="string">'com.tencent.mm'</span></span><br><span class="line">APP_ACTIVITY = <span class="string">'.ui.LauncherUI'</span></span><br><span class="line">DRIVER_SERVER = <span class="string">'http://localhost:4723/wd/hub'</span></span><br><span class="line">TIMEOUT = <span class="number">300</span></span><br><span class="line">MONGO_URL = <span class="string">'localhost'</span></span><br><span class="line">MONGO_DB = <span class="string">'moments'</span></span><br><span class="line">MONGO_COLLECTION = <span class="string">'moments'</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Moments</span>():</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">        <span class="string">""</span><span class="string">"初始化"</span><span class="string">""</span></span><br><span class="line">        <span class="comment"># 驱动配置</span></span><br><span class="line">        <span class="keyword">self</span>.desired_caps = &#123;</span><br><span class="line">            <span class="string">'platformName'</span>: PLATFORM,</span><br><span class="line">            <span class="string">'deviceName'</span>: DEVICE_NAME,</span><br><span class="line">            <span class="string">'appPackage'</span>: APP_PACKAGE,</span><br><span class="line">            <span class="string">'appActivity'</span>: APP_ACTIVITY</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">self</span>.driver = webdriver.Remote(DRIVER_SERVER, <span class="keyword">self</span>.desired_caps)</span><br><span class="line">        <span class="keyword">self</span>.wait = WebDriverWait(<span class="keyword">self</span>.driver, TIMEOUT)</span><br><span class="line">        <span class="keyword">self</span>.client = MongoClient(MONGO_URL)</span><br><span class="line">        <span class="keyword">self</span>.db = <span class="keyword">self</span>.client[MONGO_DB]</span><br><span class="line">        <span class="keyword">self</span>.collection = <span class="keyword">self</span>.db[MONGO_COLLECTION]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里实现了一些初始化配置，如驱动的配置、延时等待配置、MongoDB 连接配置等。</p>
                  <h3 id="4-模拟登录"><a href="#4-模拟登录" class="headerlink" title="4. 模拟登录"></a>4. 模拟登录</h3>
                  <p>接下来要做的就是登录微信。点击登录按钮，输入用户名、密码，提交登录即可。实现样例如下所示：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def login(self):</span><br><span class="line">    # 登录按钮</span><br><span class="line">    login = self.wait.until(<span class="module-access"><span class="module"><span class="identifier">EC</span>.</span></span>presence<span class="constructor">_of_element_located((By.ID, '<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">cjk</span>')</span>))</span><br><span class="line">    login.click<span class="literal">()</span></span><br><span class="line">    # 手机输入</span><br><span class="line">    phone = self.wait.until(<span class="module-access"><span class="module"><span class="identifier">EC</span>.</span></span>presence<span class="constructor">_of_element_located((By.ID, '<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">h2</span>')</span>))</span><br><span class="line">    phone.set<span class="constructor">_text(USERNAME)</span></span><br><span class="line">    # 下一步</span><br><span class="line">    next = self.wait.until(<span class="module-access"><span class="module"><span class="identifier">EC</span>.</span></span>element<span class="constructor">_to_be_clickable((By.ID, '<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">adj</span>')</span>))</span><br><span class="line">    next.click<span class="literal">()</span></span><br><span class="line">    # 密码</span><br><span class="line">    password = self.wait.until(<span class="module-access"><span class="module"><span class="identifier">EC</span>.</span></span>presence<span class="constructor">_of_element_located((By.XPATH, '<span class="operator">/</span><span class="operator">/</span><span class="operator">*</span>[@<span class="params">resource</span>-<span class="params">id</span>=<span class="string">"com.tencent.mm:id/h2"</span>][1]')</span>))</span><br><span class="line">    password.set<span class="constructor">_text(PASSWORD)</span></span><br><span class="line">    # 提交</span><br><span class="line">    submit = self.wait.until(<span class="module-access"><span class="module"><span class="identifier">EC</span>.</span></span>element<span class="constructor">_to_be_clickable((By.ID, '<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">adj</span>')</span>))</span><br><span class="line">    submit.click<span class="literal">()</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里依次实现了一些点击和输入操作，思路比较简单。对于不同的平台和版本来说，流程可能不太一致，这里仅作参考。</p>
                  <p>登录完成之后，进入朋友圈的页面。选中朋友圈所在的选项卡，点击朋友圈按钮，即可进入朋友圈，代码实现如下所示：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def enter(self):</span><br><span class="line">    # 选项卡</span><br><span class="line">    tab = self.wait.until(<span class="module-access"><span class="module"><span class="identifier">EC</span>.</span></span>presence<span class="constructor">_of_element_located((By.XPATH, '<span class="operator">/</span><span class="operator">/</span><span class="operator">*</span>[@<span class="params">resource</span>-<span class="params">id</span>=<span class="string">"com.tencent.mm:id/bw3"</span>][3]')</span>))</span><br><span class="line">    tab.click<span class="literal">()</span></span><br><span class="line">    # 朋友圈</span><br><span class="line">    moments = self.wait.until(<span class="module-access"><span class="module"><span class="identifier">EC</span>.</span></span>presence<span class="constructor">_of_element_located((By.ID, '<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">atz</span>')</span>))</span><br><span class="line">    moments.click<span class="literal">()</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>抓取工作正式开始。</p>
                  <h3 id="5-抓取动态"><a href="#5-抓取动态" class="headerlink" title="5. 抓取动态"></a>5. 抓取动态</h3>
                  <p>我们知道朋友圈可以一直拖动、不断刷新，所以这里需要模拟一个无限拖动的操作，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment"># 滑动点</span></span><br><span class="line">FLICK_START_X = <span class="number">300</span></span><br><span class="line">FLICK_START_Y = <span class="number">300</span></span><br><span class="line">FLICK_DISTANCE = <span class="number">700</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">crawl</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="keyword">while</span> <span class="symbol">True:</span></span><br><span class="line">        <span class="comment"># 上滑</span></span><br><span class="line">        <span class="keyword">self</span>.driver.swipe(FLICK_START_X, FLICK_START_Y + FLICK_DISTANCE, FLICK_START_X, FLICK_START_Y)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们利用 swipe() 方法，传入起始和终止点实现拖动，加入无限循环实现无限拖动。</p>
                  <p>获取当前显示的朋友圈的每条状态对应的区块元素，遍历每个区块元素，再获取内部显示的用户名、正文和发布时间，代码实现如下所示：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"># 当前页面显示的所有状态</span><br><span class="line">items = self.wait.until(</span><br><span class="line">    <span class="module-access"><span class="module"><span class="identifier">EC</span>.</span></span>presence<span class="constructor">_of_all_elements_located((By.XPATH, '<span class="operator">/</span><span class="operator">/</span><span class="operator">*</span>[@<span class="params">resource</span>-<span class="params">id</span>=<span class="string">"com.tencent.mm:id/cve"</span>]<span class="operator">/</span><span class="operator">/</span><span class="params">android</span>.<span class="params">widget</span>.FrameLayout')</span>))</span><br><span class="line"># 遍历每条状态</span><br><span class="line">for item <span class="keyword">in</span> items:</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        # 昵称</span><br><span class="line">        nickname = item.find<span class="constructor">_element_by_id('<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">aig</span>')</span>.get<span class="constructor">_attribute('<span class="params">text</span>')</span></span><br><span class="line">        # 正文</span><br><span class="line">        content = item.find<span class="constructor">_element_by_id('<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">cwm</span>')</span>.get<span class="constructor">_attribute('<span class="params">text</span>')</span></span><br><span class="line">        # 日期</span><br><span class="line">        date = item.find<span class="constructor">_element_by_id('<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">crh</span>')</span>.get<span class="constructor">_attribute('<span class="params">text</span>')</span></span><br><span class="line">        # 处理日期</span><br><span class="line">        date = self.processor.date(date)</span><br><span class="line">        print(nickname, content, date)</span><br><span class="line">        data = &#123;</span><br><span class="line">            'nickname': nickname,</span><br><span class="line">            'content': content,</span><br><span class="line">            'date': date,</span><br><span class="line">        &#125;   </span><br><span class="line">    except NoSuchElementException:</span><br><span class="line">        pass</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里遍历每条状态，再调用 find_element_by_id() 方法获取昵称、正文、发布日期对应的元素，然后通过 get_attribute() 方法获取内容。这样我们就成功获取到朋友圈的每条动态信息。</p>
                  <p>针对日期的处理，我们调用了一个 Processor 类的 date() 处理方法，该方法实现如下所示：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def <span class="type">date</span>(self, datetime):</span><br><span class="line">    """</span><br><span class="line">    处理时间</span><br><span class="line">    :param datetime: 原始时间</span><br><span class="line">    :return: 处理后时间</span><br><span class="line">    """</span><br><span class="line">    <span class="keyword">if</span> re.match(<span class="string">'d + 分钟前 '</span>, datetime):</span><br><span class="line">        minute = re.match(<span class="string">'(d+)'</span>, datetime).<span class="keyword">group</span>(<span class="number">1</span>)</span><br><span class="line">        datetime = <span class="type">time</span>.strftime(<span class="string">'% Y-% m-% d'</span>, <span class="type">time</span>.<span class="built_in">localtime</span>(<span class="type">time</span>.time() - <span class="type">float</span>(minute) * <span class="number">60</span>))</span><br><span class="line">    <span class="keyword">if</span> re.match(<span class="string">'d + 小时前 '</span>, datetime):</span><br><span class="line">        hour = re.match(<span class="string">'(d+)'</span>, datetime).<span class="keyword">group</span>(<span class="number">1</span>)</span><br><span class="line">        datetime = <span class="type">time</span>.strftime(<span class="string">'% Y-% m-% d'</span>, <span class="type">time</span>.<span class="built_in">localtime</span>(<span class="type">time</span>.time() - <span class="type">float</span>(hour) * <span class="number">60</span> * <span class="number">60</span>))</span><br><span class="line">    <span class="keyword">if</span> re.match(<span class="string">' 昨天 '</span>, datetime):</span><br><span class="line">        datetime = <span class="type">time</span>.strftime(<span class="string">'% Y-% m-% d'</span>, <span class="type">time</span>.<span class="built_in">localtime</span>(<span class="type">time</span>.time() - <span class="number">24</span> * <span class="number">60</span> * <span class="number">60</span>))</span><br><span class="line">    <span class="keyword">if</span> re.match(<span class="string">'d + 天前 '</span>, datetime):</span><br><span class="line">        day = re.match(<span class="string">'(d+)'</span>, datetime).<span class="keyword">group</span>(<span class="number">1</span>)</span><br><span class="line">        datetime = <span class="type">time</span>.strftime(<span class="string">'% Y-% m-% d'</span>, <span class="type">time</span>.<span class="built_in">localtime</span>(<span class="type">time</span>.time()) - <span class="type">float</span>(day) * <span class="number">24</span> * <span class="number">60</span> * <span class="number">60</span>)</span><br><span class="line">    <span class="keyword">return</span> datetime</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这个方法使用了正则匹配的方法来提取时间中的具体数值，再利用时间转换函数实现时间的转换。例如时间是 5 分钟前，这个方法先将 5 提取出来，用当前时间戳减去 300 即可得到发布时间的时间戳，然后再转化为标准时间即可。</p>
                  <p>最后调用 MongoDB 的 API 来实现爬取结果的存储。为了去除重复，这里调用了 update() 方法，实现如下所示：</p>
                  <figure class="highlight lasso">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="built_in">self</span>.collection.update(&#123;<span class="string">'nickname'</span>: nickname, <span class="string">'content'</span>: content&#125;, &#123;<span class="string">'$set'</span>: <span class="built_in">data</span>&#125;, <span class="literal">True</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>首先根据昵称和正文来查询信息，如果信息不存在，则插入数据，否则更新数据。这个操作的关键点是第三个参数 True，此参数设置为 True，这可以实现存在即更新、不存在则插入的操作。</p>
                  <p>最后实现一个入口方法调用以上的几个方法。调用此方法即可开始爬取，代码实现如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">main</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="comment"># 登录</span></span><br><span class="line">    <span class="keyword">self</span>.login()</span><br><span class="line">    <span class="comment"># 进入朋友圈</span></span><br><span class="line">    <span class="keyword">self</span>.enter()</span><br><span class="line">    <span class="comment"># 爬取</span></span><br><span class="line">    <span class="keyword">self</span>.crawl()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我们就完成了整个朋友圈的爬虫。代码运行之后，手机微信便会启动，并且可以成功进入到朋友圈然后一直不断执行拖动过程。控制台输出相应的爬取结果，结果被成功保存到 MongoDB 数据库中。</p>
                  <h3 id="6-结果查看"><a href="#6-结果查看" class="headerlink" title="6. 结果查看"></a>6. 结果查看</h3>
                  <p>我们到 MongoDB 中查看爬取结果，如图 11-46 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033459.jpg" alt=""></p>
                  <p>可以看到朋友圈的数据就成功保存到了数据库。</p>
                  <h3 id="7-本节代码"><a href="#7-本节代码" class="headerlink" title="7. 本节代码"></a>7. 本节代码</h3>
                  <p>本节源代码地址为：<a href="https://github.com/Python3WebSpider/Moments" target="_blank" rel="noopener"></a><a href="https://github.com/Python3WebSpider/Moments" target="_blank" rel="noopener">https://github.com/Python3WebSpider/Moments</a>。</p>
                  <h3 id="8-结语"><a href="#8-结语" class="headerlink" title="8. 结语"></a>8. 结语</h3>
                  <p>以上内容是利用 Appium 爬取微信朋友圈的过程。利用 Appium，我们可以做到 App 的可见即可爬，也可以实现自动化驱动和数据爬取。但是实际运行之后，Appium 的解析比较烦琐，而且容易发生重复和中断。如果我们可以用前文所说的 mitmdump 来监听 App 数据实时处理，而 Appium 只负责自动化驱动，它们各负其责，那么整个爬取效率和解析效率就会高很多。所以下一节我们会了解，将 mitmdump 和 Appium 结合起来爬取京东商品的过程。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-28 09:29:16" itemprop="dateCreated datePublished" datetime="2019-11-28T09:29:16+08:00">2019-11-28</time>
                </span>
                <span id="/8293.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 11.5-Appium 爬取微信朋友圈" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>4.8k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>4 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8290.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8290.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 11.4-Appium 的基本使用</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="11-4-Appium-的基本使用"><a href="#11-4-Appium-的基本使用" class="headerlink" title="11.4 Appium 的基本使用"></a>11.4 Appium 的基本使用</h1>
                  <p>Appium 是一个跨平台移动端自动化测试工具，可以非常便捷地为 iOS 和 Android 平台创建自动化测试用例。它可以模拟 App 内部的各种操作，如点击、滑动、文本输入等，只要我们手工操作的动作 Appium 都可以完成。在前面我们了解过 Selenium，它是一个网页端的自动化测试工具。Appium 实际上继承了 Selenium，Appium 也是利用 WebDriver 来实现 App 的自动化测试。对 iOS 设备来说，Appium 使用 UIAutomation 来实现驱动。对于 Android 来说，它使用 UiAutomator 和 Selendroid 来实现驱动。</p>
                  <p>Appium 相当于一个服务器，我们可以向 Appium 发送一些操作指令，Appium 就会根据不同的指令对移动设备进行驱动，完成不同的动作。</p>
                  <p>对于爬虫来说，我们用 Selenium 来抓取 JavaScript 渲染的页面，可见即可爬。Appium 同样也可以，用 Appium 来做 App 爬虫不失为一个好的选择。</p>
                  <p>下面我们来了解 Appium 的基本使用方法。</p>
                  <h3 id="1-本节目标"><a href="#1-本节目标" class="headerlink" title="1. 本节目标"></a>1. 本节目标</h3>
                  <p>我们以 Android 平台的微信为例来演示 Appium 启动和操作 App 的方法，主要目的是了解利用 Appium 进行自动化测试的流程以及相关 API 的用法。</p>
                  <h3 id="2-准备工作"><a href="#2-准备工作" class="headerlink" title="2. 准备工作"></a>2. 准备工作</h3>
                  <p>请确保 PC 已经安装好 Appium、Android 开发环境和 Python 版本的 Appium API，安装方法可以参考第 1 章。另外，Android 手机安装好微信 App。</p>
                  <h3 id="3-启动-APP"><a href="#3-启动-APP" class="headerlink" title="3. 启动 APP"></a>3. 启动 APP</h3>
                  <p>Appium 启动 App 的方式有两种：一种是用 Appium 内置的驱动器来打开 App，另一种是利用 Python 程序实现此操作。下面我们分别进行说明。</p>
                  <p>首先打开 Appium，启动界面如图 11-37 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033339.png" alt=""></p>
                  <p>图 11-37 Appium 启动界面</p>
                  <p>直接点击 Start Server 按钮即可启动 Appium 的服务，相当于开启了一个 Appium 服务器。我们可以通过 Appium 内置的驱动或 Python 代码向 Appium 的服务器发送一系列操作指令，Appium 就会根据不同的指令对移动设备进行驱动，完成不同的动作。启动后运行界面如图 11-38 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033346.jpg" alt=""></p>
                  <p>图 11-38 Server 运行界面</p>
                  <p>Appium 运行之后正在监听 4723 端口。我们可以向此端口对应的服务接口发送操作指令，此页面就会显示这个过程的操作日志。</p>
                  <p>将 Android 手机通过数据线和运行 Appium 的 PC 相连，同时打开 USB 调试功能，确保 PC 可以连接到手机。</p>
                  <p>可以输入 adb 命令来测试连接情况，如下所示：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">adb devices -l</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>如果出现类似如下结果，这就说明 PC 已经正确连接手机。</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="selector-tag">List</span> <span class="selector-tag">of</span> <span class="selector-tag">devices</span> <span class="selector-tag">attached</span></span><br><span class="line">2<span class="selector-tag">da42ac0</span> <span class="selector-tag">device</span> <span class="selector-tag">usb</span><span class="selector-pseudo">:336592896X</span> <span class="selector-tag">product</span><span class="selector-pseudo">:leo</span> <span class="selector-tag">model</span><span class="selector-pseudo">:MI_NOTE_Pro</span> <span class="selector-tag">device</span><span class="selector-pseudo">:leo</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>model 是设备的名称，就是后文需要用到的 deviceName 变量。我使用的是小米 Note 顶配版，所以此处名称为 MI_NOTE_Pro。</p>
                  <p>如果提示找不到 adb 命令，请检查 Android 开发环境和环境变量是否配置成功。如果可以成功调用 adb 命令但不显示设备信息，请检查手机和 PC 的连接情况。</p>
                  <p>接下来用 Appium 内置的驱动器打开 App，点击 Appium 中的 Start New Session 按钮，如图 11-39 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033354.jpg" alt=""></p>
                  <p>图 11-39 操作示例</p>
                  <p>这时会出现一个配置页面，如图 11-40 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033359.jpg" alt=""></p>
                  <p>图 11-40 配置页面</p>
                  <p>需要配置启动 App 时的 Desired Capabilities 参数，它们分别是 platformName、deviceName、appPackage、appActivity。</p>
                  <ul>
                    <li>platformName，平台名称，需要区分是 Android 还是 iOS，此处填写 Android。</li>
                    <li>deviceName，设备名称，是手机的具体类型。</li>
                    <li>appPackage，APP 程序包名。</li>
                    <li>appActivity，入口 Activity 名，这里通常需要以。开头。</li>
                  </ul>
                  <p>在当前配置页面的左下角也有配置参数的相关说明，链接是 <a href="https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md" target="_blank" rel="noopener"></a><a href="https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md" target="_blank" rel="noopener">https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md</a>。</p>
                  <p>我们在 Appium 中加入上面 4 个配置，如图 11-41 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033406.jpg" alt=""></p>
                  <p>图 11-41 配置信息</p>
                  <p>点击保存按钮，保存下来，我们以后可以继续使用这个配置。</p>
                  <p>点击右下角的 Start Session 按钮，即可启动 Android 手机上的微信 App 并进入到启动页面。同时 PC 上会弹出一个调试窗口，从这个窗口我们可以预览当前手机页面，并可以查看页面的源码，如图 11-42 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033409.jpg" alt=""></p>
                  <p>图 11-42 调试窗口</p>
                  <p>点击左栏中屏幕的某个元素，如选中登录按钮，它就会高亮显示。这时中间栏就显示了当前选中的按钮对应的源代码，右栏则显示了该元素的基本信息，如元素的 id、class、text 等，以及可以执行的操作，如 Tap、Send Keys、Clear，如图 11-43 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033414.jpg" alt=""></p>
                  <p>图 11-43 操作选项</p>
                  <p>点击中间栏最上方的第三个录制按钮，Appium 会开始录制操作动作，这时我们在窗口中操作 App 的行为都会被记录下来，Recorder 处可以自动生成对应语言的代码。例如，我们点击录制按钮，然后选中 App 中的登录按钮，点击 Tap 操作，即模拟了按钮点击功能，这时手机和窗口的 App 都会跳转到登录页面，同时中间栏会显示此动作对应的代码，如图 11-44 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033421.jpg" alt=""></p>
                  <p>图 11-44 录制动作</p>
                  <p>接下来选中左侧的手机号文本框，点击 Send Keys，对话框就会弹出。输入手机号，点击 Send Keys，即可完成文本的输入，如图 11-45 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-033426.jpg" alt=""></p>
                  <p>图 11-45 文本输入</p>
                  <p>我们可以在此页面点击不同的动作按钮，即可实现对 App 的控制，同时 Recorder 部分也可以生成对应的 Python 代码。</p>
                  <p>下面我们看看使用 Python 代码驱动 App 的方法。首先需要在代码中指定一个 Appium Server，而这个 Server 在刚才打开 Appium 的时候就已经开启了，是在 4723 端口上运行的，配置如下所示：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">server</span> = <span class="string">'http://localhost:4723/wd/hub'</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>用字典来配置 Desired Capabilities 参数，代码如下所示：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">desired_caps</span> = &#123;</span><br><span class="line">    <span class="string">'platformName'</span>: <span class="string">'Android'</span>,</span><br><span class="line">    <span class="string">'deviceName'</span>: <span class="string">'MI_NOTE_Pro'</span>,</span><br><span class="line">    <span class="string">'appPackage'</span>: <span class="string">'com.tencent.mm'</span>,</span><br><span class="line">    <span class="string">'appActivity'</span>: <span class="string">'.ui.LauncherUI'</span></span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>新建一个 Session，这类似点击 Appium 内置驱动的 Start Session 按钮相同的功能，代码实现如下所示：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> appium <span class="keyword">import</span> webdriver</span><br><span class="line"><span class="keyword">from</span> selenium.webdriver.support.ui <span class="keyword">import</span> WebDriverWait</span><br><span class="line"></span><br><span class="line">driver = webdriver.Remote(<span class="keyword">server</span>, desired_caps)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>配置完成后运行，就可以启动微信 App 了。但是现在仅仅是可以启动 App，还没有做任何动作。</p>
                  <p>再用代码来模拟刚才演示的两个动作：一个是点击 “登录” 按钮，一个是输入手机号。</p>
                  <p>看看刚才 Appium 内置驱动器内的 Recorder 录制生成的 Python 代码，自动生成的代码非常累赘，例如点击 “登录” 按钮的代码如下所示：</p>
                  <figure class="highlight abnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">el1</span> = driver.find_element_by_xpath(<span class="string">"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.View/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.Button[1]"</span>)</span><br><span class="line">el1.click()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这段代码的 XPath 选择器路径太长，选择方式没有那么科学，获取元素时也没有设置等待，很可能会有超时异常。所以我们修改一下，将其修改为通过 ID 查找元素，设置延时等待，两次操作的代码改写如下所示：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">wait = <span class="constructor">WebDriverWait(<span class="params">driver</span>, 30)</span></span><br><span class="line">login = wait.until(<span class="module-access"><span class="module"><span class="identifier">EC</span>.</span></span>presence<span class="constructor">_of_element_located((By.ID, '<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">cjk</span>')</span>))</span><br><span class="line">login.click<span class="literal">()</span></span><br><span class="line">phone = wait.until(<span class="module-access"><span class="module"><span class="identifier">EC</span>.</span></span>presence<span class="constructor">_of_element_located((By.ID, '<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">h2</span>')</span>))</span><br><span class="line">phone.set<span class="constructor">_text('18888888888')</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>综上所述，完整的代码如下所示：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> appium <span class="keyword">import</span> webdriver</span><br><span class="line"><span class="keyword">from</span> selenium.webdriver.common.<span class="keyword">by</span> <span class="keyword">import</span> <span class="keyword">By</span></span><br><span class="line"><span class="keyword">from</span> selenium.webdriver.support.ui <span class="keyword">import</span> WebDriverWait</span><br><span class="line"><span class="keyword">from</span> selenium.webdriver.support <span class="keyword">import</span> expected_conditions <span class="keyword">as</span> EC</span><br><span class="line"></span><br><span class="line"><span class="keyword">server</span> = <span class="string">'http://localhost:4723/wd/hub'</span></span><br><span class="line">desired_caps = &#123;</span><br><span class="line">    <span class="string">'platformName'</span>: <span class="string">'Android'</span>,</span><br><span class="line">    <span class="string">'deviceName'</span>: <span class="string">'MI_NOTE_Pro'</span>,</span><br><span class="line">    <span class="string">'appPackage'</span>: <span class="string">'com.tencent.mm'</span>,</span><br><span class="line">    <span class="string">'appActivity'</span>: <span class="string">'.ui.LauncherUI'</span></span><br><span class="line">&#125;</span><br><span class="line">driver = webdriver.Remote(<span class="keyword">server</span>, desired_caps)</span><br><span class="line">wait = WebDriverWait(driver, <span class="number">30</span>)</span><br><span class="line"><span class="keyword">login</span> = wait.<span class="keyword">until</span>(EC.presence_of_element_located((<span class="keyword">By</span>.ID, <span class="string">'com.tencent.mm:id/cjk'</span>)))</span><br><span class="line"><span class="keyword">login</span>.click()</span><br><span class="line">phone = wait.<span class="keyword">until</span>(EC.presence_of_element_located((<span class="keyword">By</span>.ID, <span class="string">'com.tencent.mm:id/h2'</span>)))</span><br><span class="line">phone.set_text(<span class="string">'18888888888'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>一定要重新连接手机，再运行此代码，这时即可观察到手机上首先弹出了微信欢迎页面，然后模拟点击登录按钮、输入手机号，操作完成。这样我们就成功使用 Python 代码实现了 App 的操作。</p>
                  <h3 id="4-API"><a href="#4-API" class="headerlink" title="4. API"></a>4. API</h3>
                  <p>接下来看看使用代码如何操作 App、总结相关 API 的用法。这里使用的 Python 库为 AppiumPythonClient，其 GitHub 地址为 <a href="https://github.com/appium/python-client，此库继承自" target="_blank" rel="noopener">https://github.com/appium/python-client，此库继承自</a> Selenium，使用方法与 Selenium 有很多共同之处。</p>
                  <h4 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h4>
                  <p>需要配置 Desired Capabilities 参数，完整的配置说明可以参考 <a href="https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md" target="_blank" rel="noopener"></a><a href="https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md" target="_blank" rel="noopener">https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md</a>，一般来说我们我们配置几个基本参数即可：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> appium import webdriver</span><br><span class="line"></span><br><span class="line">server = <span class="string">'http://localhost:4723/wd/hub'</span></span><br><span class="line">desired_caps = &#123;</span><br><span class="line">    <span class="string">'platformName'</span>: <span class="string">'Android'</span>,</span><br><span class="line">    <span class="string">'deviceName'</span>: <span class="string">'MI_NOTE_Pro'</span>,</span><br><span class="line">    <span class="string">'appPackage'</span>: <span class="string">'com.tencent.mm'</span>,</span><br><span class="line">    <span class="string">'appActivity'</span>: <span class="string">'.ui.LauncherUI'</span></span><br><span class="line">&#125;</span><br><span class="line">driver = webdriver.Remote(server, desired_caps)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里配置了启动微信 App 的 Desired Capabilities，这样 Appnium 就会自动查找手机上的包名和入口类，然后将其启动。包名和入口类的名称可以在安装包中的 AndroidManifest.xml 文件获取。</p>
                  <p>如果要打开的 App 没有事先在手机上安装，我们可以直接指定 App 参数为安装包所在路径，这样程序启动时就会自动向手机安装并启动 App，如下所示：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> appium import webdriver</span><br><span class="line"></span><br><span class="line">server = <span class="string">'http://localhost:4723/wd/hub'</span></span><br><span class="line">desired_caps = &#123;</span><br><span class="line">    <span class="string">'platformName'</span>: <span class="string">'Android'</span>,</span><br><span class="line">    <span class="string">'deviceName'</span>: <span class="string">'MI_NOTE_Pro'</span>,</span><br><span class="line">    <span class="string">'app'</span>: <span class="string">'./weixin.apk'</span></span><br><span class="line">&#125;</span><br><span class="line">driver = webdriver.Remote(server, desired_caps)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>程序启动的时候就会寻找 PC 当前路径下的 APK 安装包，然后将其安装到手机中并启动。</p>
                  <h4 id="查找元素"><a href="#查找元素" class="headerlink" title="查找元素"></a>查找元素</h4>
                  <p>我们可以使用 Selenium 中通用的查找方法来实现元素的查找，如下所示：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">el = driver.find<span class="constructor">_element_by_id('<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">cjk</span>')</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在 Selenium 中，其他查找元素的方法同样适用，在此不再赘述。</p>
                  <p>在 Android 平台上，我们还可以使用 UIAutomator 来进行元素选择，如下所示：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">el</span> = self.driver.find_element_by_android_uiautomator(<span class="string">'new UiSelector().description("Animation")'</span>)</span><br><span class="line"><span class="attr">els</span> = self.driver.find_elements_by_android_uiautomator(<span class="string">'new UiSelector().clickable(true)'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在 iOS 平台上，我们可以使用 UIAutomation 来进行元素选择，如下所示：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">el</span> = self.driver.find_element_by_ios_uiautomation(<span class="string">'.elements()[0]'</span>)</span><br><span class="line"><span class="attr">els</span> = self.driver.find_elements_by_ios_uiautomation(<span class="string">'.elements()'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>还可以使用 iOS Predicates 来进行元素选择，如下所示：</p>
                  <figure class="highlight nix">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">el</span> = self.driver.find_element_by_ios_predicate('<span class="attr">wdName</span> == <span class="string">"Buttons"</span>')</span><br><span class="line"><span class="attr">els</span> = self.driver.find_elements_by_ios_predicate('<span class="attr">wdValue</span> == <span class="string">"SearchBar"</span> AND <span class="attr">isWDDivisible</span> == <span class="number">1</span>')</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>也可以使用 iOS Class Chain 来进行选择，如下所示：</p>
                  <figure class="highlight ceylon">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">el = self.driver.find<span class="number">_</span>element<span class="number">_</span><span class="meta">by</span><span class="number">_</span>ios<span class="number">_</span><span class="keyword">class</span><span class="number">_</span>chain(<span class="string">'XCUIElementTypeWindow/XCUIElementTypeButton[3]'</span>)</span><br><span class="line">els = self.driver.find<span class="number">_</span>elements<span class="number">_</span><span class="meta">by</span><span class="number">_</span>ios<span class="number">_</span><span class="keyword">class</span><span class="number">_</span>chain(<span class="string">'XCUIElementTypeWindow/XCUIElementTypeButton'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>但是此种方法只适用于 XCUITest 驱动，具体可以参考：<a href="https://github.com/appium/appium-xcuitest" target="_blank" rel="noopener">https://github.com/appium/appium-xcuitest</a>-<br>driver。</p>
                  <h4 id="点击"><a href="#点击" class="headerlink" title="点击"></a>点击</h4>
                  <p>点击可以使用 tap() 方法，该方法可以模拟手指点击（最多五个手指），可设置按时长短（毫秒），代码如下所示：</p>
                  <figure class="highlight lasso">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">tap(<span class="built_in">self</span>, positions, <span class="built_in">duration</span>=<span class="literal">None</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>参数：</p>
                  <ul>
                    <li>positions，点击的位置组成的列表。</li>
                    <li>duration，点击持续时间。</li>
                  </ul>
                  <p>实例如下：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">driver.tap([(<span class="number">100</span>, <span class="number">20</span>), (<span class="number">100</span>, <span class="number">60</span>), (<span class="number">100</span>, <span class="number">100</span>)], <span class="number">500</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样就可以模拟点击屏幕的某几个点。</p>
                  <p>另外对于某个元素如按钮来说，我们可以直接调用 cilck() 方法实现模拟点击，实例如下所示：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">button = find<span class="constructor">_element_by_id('<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">btn</span>')</span></span><br><span class="line">button.click<span class="literal">()</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样获取元素之后，然后调用 click() 方法即可实现该元素的模拟点击。</p>
                  <h4 id="屏幕拖动"><a href="#屏幕拖动" class="headerlink" title="屏幕拖动"></a>屏幕拖动</h4>
                  <p>可以使用 scroll() 方法模拟屏幕滚动，用法如下所示：</p>
                  <figure class="highlight stylus">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="title">scroll</span><span class="params">(self, origin_el, destination_el)</span></span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以实现从元素 origin_el 滚动至元素 destination_el。</p>
                  <p>参数：</p>
                  <ul>
                    <li>original_el，被操作的元素</li>
                    <li>destination_el，目标元素</li>
                  </ul>
                  <p>实例如下：</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="selector-tag">driver</span><span class="selector-class">.scroll</span>(<span class="selector-tag">el1</span>,<span class="selector-tag">el2</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们还可以使用 swipe() 模拟从 A 点滑动到 B 点，用法如下：</p>
                  <figure class="highlight lasso">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">swipe(<span class="built_in">self</span>, start_x, start_y, end_x, end_y, <span class="built_in">duration</span>=<span class="literal">None</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>参数：</p>
                  <ul>
                    <li>start_x，开始位置的横坐标</li>
                    <li>start_y，开始位置的纵坐标</li>
                    <li>end_x，终止位置的横坐标</li>
                    <li>end_y，终止位置的纵坐标</li>
                    <li>duration，持续时间，毫秒</li>
                  </ul>
                  <p>实例如下：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">driver.swipe(<span class="number">100</span>, <span class="number">100</span>, <span class="number">100</span>, <span class="number">400</span>, <span class="number">5000</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样可以实现在 5s 由 (100, 100) 滑动到 (100, 400)。</p>
                  <p>另外可以使用 flick() 方法模拟从 A 点快速滑动到 B 点，用法如下：</p>
                  <figure class="highlight stylus">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="title">flick</span><span class="params">(self, start_x, start_y, end_x, end_y)</span></span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>参数：</p>
                  <ul>
                    <li>start_x，开始位置的横坐标</li>
                    <li>start_y，开始位置的纵坐标</li>
                    <li>end_x，终止位置的横坐标</li>
                    <li>end_y，终止位置的纵坐标</li>
                  </ul>
                  <p>实例如下：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">driver.flick(<span class="number">100</span>, <span class="number">100</span>, <span class="number">100</span>, <span class="number">400</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h4 id="拖拽"><a href="#拖拽" class="headerlink" title="拖拽"></a>拖拽</h4>
                  <p>可以使用 drag_and_drop() 实现某个元素拖动到另一个目标元素上。</p>
                  <p>用法如下：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">drag<span class="constructor">_and_drop(<span class="params">self</span>, <span class="params">origin_el</span>, <span class="params">destination_el</span>)</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以实现元素 origin_el 拖拽至元素 destination_el。</p>
                  <p>参数：</p>
                  <ul>
                    <li>original_el，被拖拽的元素</li>
                    <li>destination_el，目标元素</li>
                  </ul>
                  <p>实例如下所示：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">driver.drag<span class="constructor">_and_drop(<span class="params">el1</span>, <span class="params">el2</span>)</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h4 id="文本输入"><a href="#文本输入" class="headerlink" title="文本输入"></a>文本输入</h4>
                  <p>可以使用 set_text() 方法实现文本输入，如下所示：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">el = find<span class="constructor">_element_by_id('<span class="params">com</span>.<span class="params">tencent</span>.<span class="params">mm</span>:<span class="params">id</span><span class="operator">/</span><span class="params">cjk</span>')</span></span><br><span class="line">el.set<span class="constructor">_text('Hello')</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们选中一个文本框元素之后，然后调用 set_text() 方法即可实现文本输入。</p>
                  <h4 id="动作链"><a href="#动作链" class="headerlink" title="动作链"></a>动作链</h4>
                  <p>与 Selenium 中的 ActionChains 类似，Appium 中的 TouchAction 可支持的方法有 tap()、press()、long_press()、release()、move_to()、wait()、cancel() 等，实例如下所示：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">el = self.driver.find_element_by_accessibility_id(<span class="string">'Animation'</span>)</span><br><span class="line">action = TouchAction(self.driver)</span><br><span class="line">action.tap(el).<span class="keyword">perform</span>()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>首先选中一个元素，然后利用 TouchAction 实现点击操作。</p>
                  <p>如果想要实现拖动操作，可以用如下方式：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">els = self.driver.find<span class="constructor">_elements_by_class_name('<span class="params">listView</span>')</span></span><br><span class="line">a1 = <span class="constructor">TouchAction()</span></span><br><span class="line">a1.press(els<span class="literal">[<span class="number">0</span>]</span>).move<span class="constructor">_to(<span class="params">x</span>=10, <span class="params">y</span>=0)</span>.move<span class="constructor">_to(<span class="params">x</span>=10, <span class="params">y</span>=-75)</span>.move<span class="constructor">_to(<span class="params">x</span>=10, <span class="params">y</span>=-600)</span>.release<span class="literal">()</span></span><br><span class="line">a2 = <span class="constructor">TouchAction()</span></span><br><span class="line">a2.press(els<span class="literal">[<span class="number">1</span>]</span>).move<span class="constructor">_to(<span class="params">x</span>=10, <span class="params">y</span>=10)</span>.move<span class="constructor">_to(<span class="params">x</span>=10, <span class="params">y</span>=-300)</span>.move<span class="constructor">_to(<span class="params">x</span>=10, <span class="params">y</span>=-600)</span>.release<span class="literal">()</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>利用以上 API，我们就可以完成绝大部分操作。更多的 API 操作可以参考 <a href="https://testerhome.com/topics/3711" target="_blank" rel="noopener"></a><a href="https://testerhome.com/topics/3711" target="_blank" rel="noopener">https://testerhome.com/topics/3711</a>。</p>
                  <h3 id="5-结语"><a href="#5-结语" class="headerlink" title="5. 结语"></a>5. 结语</h3>
                  <p>本节中，我们主要了解了 Appium 的操作 App 的基本用法，以及常用 API 的用法。在下一节我们会用一个实例来演示 Appium 的使用方法。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-28 09:24:05" itemprop="dateCreated datePublished" datetime="2019-11-28T09:24:05+08:00">2019-11-28</time>
                </span>
                <span id="/8290.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 11.4-Appium 的基本使用" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>8.4k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>8 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8272.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8272.html" class="post-title-link" itemprop="url">X-Forward-For 看破红尘，代理 IP 无所遁形！</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p><img src="http://q0revehsm.bkt.clouddn.com/sfhfpc/20191124150957.png" alt=""> 在开始了解 X-Forward-For 之前，我们先来假设一个场景。你是一名爬虫工程师，现在要爬取目标网站 xxx.com 上面的内容。在编码的时候，你发现单位时间内请求频率过高时会被限制，猜测应该是<strong>目标网站针对 IP 地址做了限制</strong>。现在你有两种选择：</p>
                  <ul>
                    <li><strong>单机，用 IP 代理解决频率高被限制的问题。</strong></li>
                    <li><strong>多机，用分布式爬虫解决单机 IP 被限制的问题。</strong></li>
                  </ul>
                  <p>由于目标网站只需要爬取一次，<strong>单机+IP 代理</strong>这种组合的成本更低，所以你选择了它。从 IP 代理服务商 xx 处购买了代理服务后，你进行了新一轮的测试，代码片段 Forwarded-Test 为测试代码。</p>
                  <figure class="highlight nix">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="built_in">import</span> requests</span><br><span class="line"></span><br><span class="line"><span class="comment"># 请求地址</span></span><br><span class="line"><span class="attr">targetUrl</span> = <span class="string">"http://111.231.93.117/"</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 代理服务器</span></span><br><span class="line"><span class="attr">proxyHost</span> = <span class="string">"220.185.128.170"</span></span><br><span class="line"><span class="attr">proxyPort</span> = <span class="string">"9999"</span></span><br><span class="line"></span><br><span class="line"><span class="attr">proxyMeta</span> = <span class="string">"http://%(host)s:%(port)s"</span> % &#123;</span><br><span class="line"></span><br><span class="line">    <span class="string">"host"</span>: proxyHost,</span><br><span class="line">    <span class="string">"port"</span>: proxyPort,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="attr">proxies</span> = &#123;</span><br><span class="line"></span><br><span class="line">    <span class="string">"http"</span>: proxyMeta,</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment"># 设定一个 Referer</span></span><br><span class="line"><span class="attr">header</span> = &#123;</span><br><span class="line">    <span class="string">"Referer"</span>: <span class="string">"http://www.sfhfpc.com"</span>,</span><br><span class="line">&#125;</span><br><span class="line"><span class="attr">resp</span> = requests.get(targetUrl, <span class="attr">proxies=proxies,</span> <span class="attr">headers=header)</span></span><br><span class="line">print(resp.status_code)</span><br><span class="line">print(resp.text)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>代码片段 Forwarded-Test 代码运行后，你发现你仍然被限制！ 顿时感到头大，于是在各大搜索引擎寻找相关资料，例如：</p>
                  <blockquote>
                    <p>ip 代理无效 识别 ip 代理 ip 代理被发现</p>
                  </blockquote>
                  <p>你发现很多文章中都提到一个东西 X-Forward-For，大家都说它能够<strong>看破</strong> IP 代理。 <img src="http://q0revehsm.bkt.clouddn.com/sfhfpc/20191127160630.jpg" alt=""> 那么问题来了：</p>
                  <ul>
                    <li>X-Forward-For 到底是什么呢？</li>
                    <li>为什么 X-Forward-For 能够发现我们使用了 <strong>IP 代理</strong></li>
                    <li>它怎么能找到<strong>原始 IP</strong> 呢？</li>
                    <li>有什么方法可以骗过 X-Forward-For 呢？</li>
                  </ul>
                  <p>带着这些问题，我们就来研究一下 X-Forward-For。</p>
                  <h2 id="X-Forward-For-是什么"><a href="#X-Forward-For-是什么" class="headerlink" title="X-Forward-For 是什么"></a>X-Forward-For 是什么</h2>
                  <p>X-Forward-For 跟 Referer 和 User-Agent 一样，都是 HTTP 中的头域。HTTP/1.1 的 <strong>RFC</strong> 文档编号为 2616，在 2616 中并未提及 X-Forward-For，也就是说 HTTP/1.1 出现的时候 X-Forward-For 还没出生。真正提出 X-Forward-For 的是2014 年的 RFC7239（详见 <a href="https://www.rfc-editor.org/rfc/rfc7239.txt），这时候" target="_blank" rel="noopener">https://www.rfc-editor.org/rfc/rfc7239.txt），这时候</a> X-Forward-For 作为<strong>HTTP 扩展</strong>出现。 <sup><a href="#fn_RFC" id="reffn_RFC">RFC</a></sup>: 全称 Request For Comments，是一系列以编号排定的文件。它收集了互联网相关的协议信息，你可以抽象地将 RFC2616 理解为 HTTP/1.1 的协议规范。Websocket 协议规范的详细解读可参考《Python3 反爬虫原理与绕过实战》一书。 关于 X-Forward-For 的所有正确描述都写在了 RFC7239 中，所有符合规范的 HTTP 也会遵守 RFC7239。当然，你也可以选择<strong>不遵守</strong>。 <sup><a href="#fn_不遵守" id="reffn_不遵守">不遵守</a></sup>: 实际上，RFC 只是一种规范、约定，作为大家统一行径的参考，并未强制实现。很多反爬虫手段就是另辟蹊径，采用了与 RFC 约定不同的策略，具体反爬虫思路和案例可参考《Python3 反爬虫原理与绕过实战》一书。 RFC7239 很长，我们不必逐一阅读。实际上跟我们相关的只有几个部分，例如：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">1.</span>Abstract</span><br><span class="line"><span class="number">7.5</span>. Example Usage</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>Abstract 是本文章的摘要，它描述了 RFC7239 的作用：</p>
                  <blockquote>
                    <p>This document defines an HTTP extension header field that allows proxy components to disclose information lost in the proxying process, for example, the originating IP address of a request or IP address of the proxy on the user-agent-facing interface. In a path of proxying components, this makes it possible to arrange it so that each subsequent component will have access to, for example, all IP addresses used in the chain of proxied HTTP requests. This document also specifies guidelines for a proxy administrator to anonymize the origin of a request.</p>
                  </blockquote>
                  <p>大体意思为本文的定义（扩展）了一个 HTTP 头域，这个字段允许代理组件披露原始 IP 地址。 从这里我们了解到 X-Forward-For 的正向用途是便于服务端识别原始 IP，并根据原始 IP 作出动态处理。例如服务端按照 IP 地址进行负载均衡时，如果能够<strong>看破</strong> IP 代理，取得原始 IP 地址，那么就能够作出<strong>有效</strong>的负载。否则有可能造成资源分配不均，导致<strong>假负载均衡</strong>的情况出现。 Example Usage 给出了 X-Forward-For 的使用示例：</p>
                  <blockquote>
                    <p>A request from a client with IP address 192.0.2.43 passes through a proxy with IP address 198.51.100.17, then through another proxy with IP address 203.0.113.60 before reaching an origin server. This could, for example, be an office client behind a corporate malware filter talking to a origin server through a reverse proxy. o The HTTP request between the client and the first proxy has no “Forwarded” header field. o The HTTP request between the first and second proxy has a “Forwarded: for=192.0.2.43” header field. o The HTTP request between the second proxy and the origin server has a “Forwarded: for=192.0.2.43, for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com” header field.</p>
                  </blockquote>
                  <p>假设原始 IP 为192.0.2.43，它的请求使用了地址为 198.51.100.17 的代理，在到达目标服务器 203.0.113.60 之前还使用了另外一个代理（文章假设另外一个代理为 222.111.222.111）。 这种情况下</p>
                  <ul>
                    <li>客户端和第一个代理之间的 HTTP 请求中没有 Forwarded 头域。</li>
                    <li>第一个代理和第二个代理之间的 HTTP 请求中有 Forwarded 头域，头域及值为 Forwarded: for=192.0.2.43 。</li>
                    <li>第二个代理和服务器之间的 HTTP 请求中有 Forwarded 头域，头域及值为 Forwarded: for=192.0.2.43, for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com”</li>
                  </ul>
                  <p>图 forwarded-client-server 描述了上述情景。 <img src="http://q0revehsm.bkt.clouddn.com/sfhfpc/20191124123035.jpg" alt=""> 图 forwarded-client-server 由于客户端到代理 1 的请求没有使用代理，所以值为空或短横线。到代理 2 时，中间经过了代理 1，所以值为原始 IP。到服务端时，中间经过了代理 1 和代理2 ，所以值为原始 IP 和代理 1 IP。 上面就是关于 RFC7239 中部分内容的解读。看到这里，想必你已有丝丝头绪，接下来我们再捋一捋。</p>
                  <h2 id="IP-代理实验"><a href="#IP-代理实验" class="headerlink" title="IP 代理实验"></a>IP 代理实验</h2>
                  <p>首先我在自己的测试服务器上安装并启动了 Nginx，它的默认日志格式如下：</p>
                  <figure class="highlight nginx">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">log_format</span>  main  </span><br><span class="line"><span class="string">'<span class="variable">$remote_addr</span> - <span class="variable">$remote_user</span> [<span class="variable">$time_local</span>] "<span class="variable">$request</span>" '</span></span><br><span class="line"><span class="string">'<span class="variable">$status</span> <span class="variable">$body_bytes_sent</span> "<span class="variable">$http_referer</span>" '</span></span><br><span class="line"><span class="string">'"<span class="variable">$http_user_agent</span>" "<span class="variable">$http_x_forwarded_for</span>"'</span>;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>即 access.log 文件中会记录客户端 IP 地址、客户端时间、请求方式、响应状态码、响应正文大小、Referer、User-Agent 和代理清单。</p>
                  <blockquote>
                    <p>提示：Nginx 中 $http_x_forwarded_for 对应的值这里称为代理清单，它与 RFC7239 中的 Forwarded 含义相同。</p>
                  </blockquote>
                  <p>当我使用计算机终端浏览器访问测试服务器地址时，对应的日志记录如下：</p>
                  <figure class="highlight accesslog">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">180.137.156.168</span> - - <span class="string">[24/Nov/2019:12:41:19 +0800]</span> <span class="string">"<span class="keyword">GET</span> / HTTP/1.1"</span> <span class="number">200</span> <span class="number">612</span> <span class="string">"-"</span> <span class="string">"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Safari/605.1.15"</span> <span class="string">"-"</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>服务器记录到的信息含义如下：</p>
                  <ul>
                    <li>客户端 IP 为 180.137.156.168</li>
                    <li>客户端时间为 [24/Nov/2019:12:41:19 +0800]</li>
                    <li>请求方式为 GET / HTTP/1.1</li>
                    <li>响应状态码为 200</li>
                    <li>响应正文大小为 612</li>
                    <li>Referer 为短横线，即为空</li>
                    <li>User-Agent 显示浏览器品牌为 Safari</li>
                    <li>代理清单为短横线，即为空。</li>
                  </ul>
                  <p>由于本次并未使用 IP 代理，那么代理清单自然就是短横线。接着我们用 Python 代码测试一下，代码片段 Python-Request 为测试代码。</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">import requests</span><br><span class="line">resp = requests.<span class="builtin-name">get</span>(<span class="string">"http://111.231.93.117/"</span>)</span><br><span class="line"><span class="builtin-name">print</span>(resp.status_code)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>代码片段 Python-Request 代码运行结果为 200，即目标服务器正确响应了本次请求。对应的日志记录如下：</p>
                  <figure class="highlight accesslog">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">180.137.156.168</span> - - <span class="string">[24/Nov/2019:12:49:41 +0800]</span> <span class="string">"<span class="keyword">GET</span> / HTTP/1.1"</span> <span class="number">200</span> <span class="number">612</span> <span class="string">"-"</span> <span class="string">"python-requests/2.21.0"</span> <span class="string">"-"</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这次也没有使用 IP 代理，所以代理清单依旧是短横线。现在用代理 IP 测试一下，代码片段 Forwarded-Test 中使用了 IP 代理，我们就用它进行测试即可。这里的代理服务器 IP 地址为 220.185.128.170，根据之前对 RFC7239 的了解，猜测本次请求对应的 Forwarded 记录的会是原始 IP，而客户端 IP 则是代理服务器的 IP。 代码运行后，服务器记录到对应的日志信息如下：</p>
                  <figure class="highlight accesslog">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">220.185.128.170</span> - - <span class="string">[24/Nov/2019:12:52:58 +0800]</span> <span class="string">"<span class="keyword">GET</span> / HTTP/1.1"</span> <span class="number">200</span> <span class="number">612</span> <span class="string">"http://www.sfhfpc.com"</span> <span class="string">"python-requests/2.21.0"</span> <span class="string">"180.137.156.168"</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>果然，记录中客户端 IP 对应的是 220.185.128.170，即代理服务器的 IP。Forwarded 中记录的 180.137.156.168 是 Python 程序所在的计算机 IP 地址，即原始 IP。 这与 RFC7239 的描述完全相符，服务端可以通过 Forwarded 找到原始 IP，甚至是使用过的代理服务器 IP。</p>
                  <h2 id="调皮的-IP-代理商"><a href="#调皮的-IP-代理商" class="headerlink" title="调皮的 IP 代理商"></a>调皮的 IP 代理商</h2>
                  <p>刚才我们用的是普通 IP 代理，由于它很容易被识别，达不到<strong>隐匿</strong>的目的，所以 IP 代理商又推出了<strong>高匿代理</strong>。 <sup><a href="#fn_高匿代理" id="reffn_高匿代理">高匿代理</a></sup>: 相对于普通 IP 代理而言，使用高匿代理后，原始 IP 会被隐藏得更好，服务端更难发现。 这里我使用了 <a href="http://h.zhimaruanjian.com/?utm-source=ggzz&amp;utm-keyword=?01" target="_blank" rel="noopener">芝麻代理</a> 服务商提供的免费高匿 IP，注册后就可以领取免费 IP，简直就是开箱即用。 <img src="http://q0revehsm.bkt.clouddn.com/sfhfpc/20191127155616.png" alt=""> 将代码片段 Forwarded-Test 中用于设置代理服务器 IP 和端口号的字段值改为高匿 IP 及对应的端口号即可，例如：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment"># 代理服务器</span></span><br><span class="line"><span class="attr">proxyHost</span> = <span class="string">"58.218.92.132"</span>  <span class="comment"># "220.185.128.170"</span></span><br><span class="line"><span class="attr">proxyPort</span> = <span class="string">"2390"</span>  <span class="comment"># "9999"</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>保存更改后运行代码，对应的日志记录如下：</p>
                  <figure class="highlight accesslog">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">125.82.188.4</span> - - <span class="string">[24/Nov/2019:13:05:07 +0800]</span> <span class="string">"<span class="keyword">GET</span> / HTTP/1.1"</span> <span class="number">200</span> <span class="number">612</span> <span class="string">"http://www.sfhfpc.com"</span> <span class="string">"python-requests/2.21.0"</span> <span class="string">"-"</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>原始 IP 为 125.82.188.4，代理清单为短横线。细心的你可能会有疑问，为什么填写的代理 IP 是 58.218.92.132，而日志中的却不是呢？ 这是代理服务商做了多一层的转移，58.218.92.132 是给用户的入口，代理商的服务端会将入口为 58.218.92.132 的请求转给地址为 125.82.188.4。其中过程我们不用深究，高匿代理和普通代理的原理会再开一篇文章进行讨论。 日志记录说明高匿 IP 能够帮助我们实现<strong>隐匿</strong>的目的。说到这里不得不提一下，芝麻代理高匿 IP 的质量真的好，听说他们的 IP 还支持高并发调用，有需求的朋友不妨去试试。</p>
                  <h2 id="机智的你和想当然的开发者"><a href="#机智的你和想当然的开发者" class="headerlink" title="机智的你和想当然的开发者"></a>机智的你和想当然的开发者</h2>
                  <p>难道普通代理就一定会被 X-Forward-For 发现吗？ 办法总是会有的，翻一下 <a href="http://www.sfhfpc.com" target="_blank" rel="noopener">http://www.sfhfpc.com</a> 或者公众号<strong>韦世东学算法和反爬虫</strong>说不定灵感就来了！在解读 RFC7239 - Example Usage 时，我们了解到 X-Forward-For 会记录原始 IP，在使用多层 IP 代理的情况下记录的是上层 IP。利用这个特点，是不是可以<strong>伪造</strong>一下呢？ 既然 X-Forward-For 和 Referer 一样是头域，那么就说明它可以被人为改变。我们只需要在请求时加上 X-Forward-For 请求头和对应的值即可。代码片段 Python-Request-CustomHeader 实现了这样的需求。</p>
                  <figure class="highlight nix">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="built_in">import</span> requests</span><br><span class="line"></span><br><span class="line"><span class="comment"># 请求地址</span></span><br><span class="line"><span class="attr">targetUrl</span> = <span class="string">"http://111.231.93.117/"</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 代理服务器</span></span><br><span class="line"><span class="attr">proxyHost</span> = <span class="string">"220.185.128.170"</span></span><br><span class="line"><span class="attr">proxyPort</span> = <span class="string">"9999"</span></span><br><span class="line"></span><br><span class="line"><span class="attr">proxyMeta</span> = <span class="string">"http://%(host)s:%(port)s"</span> % &#123;</span><br><span class="line"></span><br><span class="line">    <span class="string">"host"</span>: proxyHost,</span><br><span class="line">    <span class="string">"port"</span>: proxyPort,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="attr">proxies</span> = &#123;</span><br><span class="line">    <span class="string">"http"</span>: proxyMeta,</span><br><span class="line">&#125;</span><br><span class="line"><span class="attr">header</span> = &#123;</span><br><span class="line">    <span class="string">"Referer"</span>: <span class="string">"http://www.sfhfpc.com"</span>,</span><br><span class="line">    <span class="string">"X-Forwarded-For"</span>: <span class="string">"_"</span>,</span><br><span class="line">&#125;</span><br><span class="line"><span class="attr">resp</span> = requests.get(targetUrl, <span class="attr">proxies=proxies,</span> <span class="attr">headers=header)</span></span><br><span class="line">print(resp.status_code)</span><br><span class="line">print(resp.text)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>代码片段 Python-Request-CustomHeader 代码运行后，控制台结果如下：</p>
                  <figure class="highlight xml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">200</span><br><span class="line"><span class="meta">&lt;!DOCTYPE <span class="meta-keyword">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">title</span>&gt;</span>Welcome to nginx!<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span>&gt;</span></span><br><span class="line">    body &#123;</span><br><span class="line">        width: 35em;</span><br><span class="line">        margin: 0 auto;</span><br><span class="line">        font-family: Tahoma, Verdana, Arial, sans-serif;</span><br><span class="line">    &#125;</span><br><span class="line"><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">h1</span>&gt;</span>Welcome to nginx!<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">p</span>&gt;</span>If you see this page, the nginx web server is successfully installed and</span><br><span class="line">working. Further configuration is required.<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">p</span>&gt;</span>For online documentation and support please refer to</span><br><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">"http://nginx.org/"</span>&gt;</span>nginx.org<span class="tag">&lt;/<span class="name">a</span>&gt;</span>.<span class="tag">&lt;<span class="name">br</span>/&gt;</span></span><br><span class="line">Commercial support is available at</span><br><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">"http://nginx.com/"</span>&gt;</span>nginx.com<span class="tag">&lt;/<span class="name">a</span>&gt;</span>.<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">p</span>&gt;</span><span class="tag">&lt;<span class="name">em</span>&gt;</span>Thank you for using nginx.<span class="tag">&lt;/<span class="name">em</span>&gt;</span><span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>响应状态码是 200，并且返回了 Welcome to nginx 等字样，这说明请求成功。对应的日志记录为：</p>
                  <figure class="highlight accesslog">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">220.185.128.170</span> - - <span class="string">[24/Nov/2019:14:13:24 +0800]</span> <span class="string">"<span class="keyword">GET</span> / HTTP/1.1"</span> <span class="number">200</span> <span class="number">612</span> <span class="string">"http://www.sfhfpc.com"</span> <span class="string">"python-requests/2.21.0"</span> <span class="string">"_, 180.137.156.168"</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>记录显示，原始 IP 为 220.185.128.170、代理清单为 “_, 180.137.156.168”。实际上原始 IP 是 180.137.156.168，而代理服务器的 IP 是 220.185.128.170。代理清单中多出来的短横线是我们在代码中加上的，这里居然也显示了。这说明我们只需要在请求时附带上 X-Forward-For 头域就可以达到<strong>伪造</strong>的目的。 如果我想让服务端认为原始 IP 为 112.113.115.116，那么只需要将代码片段 Python-Request-CustomHeader 中 header 对象中 X-Forwarded-For 键对应的值设置为 112.113.115.116 即可。 保存后运行代码，对应的日志记录如下：</p>
                  <figure class="highlight accesslog">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">220.185.128.170</span> - - <span class="string">[24/Nov/2019:14:28:08 +0800]</span> <span class="string">"<span class="keyword">GET</span> / HTTP/1.1"</span> <span class="number">200</span> <span class="number">612</span> <span class="string">"http://www.sfhfpc.com"</span> <span class="string">"python-requests/2.21.0"</span> <span class="string">"112.113.115.116, 180.137.156.168"</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>根据 RFC7239 - Example Usage，开发者会认为代理清单中的第一组 IP 地址是原始 IP，殊不知这是我们特意为他准备的。</p>
                  <h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2>
                  <p>X-Forward-For 是 HTTP 协议扩展的一个头域，它可以识别出经过多层代理后的原始 IP。捣蛋的人向来不喜欢遵守约定和规范，来了个鱼目混珠。更多关于 RFC 协议解读和通过违反约定实现的反爬虫措施可翻阅《Python3 反爬虫原理与绕过实战》一书。 <strong>提示：点击链接「<a href="http://h.zhimaruanjian.com/?utm-source=ggzz&amp;utm-keyword=?01" target="_blank" rel="noopener">免费领 IP</a>」可前往芝麻代理领取免费 IP。</strong> <strong>版权声明</strong> 作者：韦世东 链接：<a href="http://www.sfhfpc.com" target="_blank" rel="noopener">http://www.sfhfpc.com</a> 来源：算法和反爬虫 著作权归作者所有，非商业转载请注明出处，禁止商业转载。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/韦世东学算法和反爬虫" class="author" itemprop="url" rel="index">韦世东学算法和反爬虫</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-27 20:06:12" itemprop="dateCreated datePublished" datetime="2019-11-27T20:06:12+08:00">2019-11-27</time>
                </span>
                <span id="/8272.html" class="post-meta-item leancloud_visitors" data-flag-title="X-Forward-For 看破红尘，代理 IP 无所遁形！" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>7.9k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>7 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8263.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8263.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 11.3-mitmdump 爬取 “得到” App 电子书信息</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="11-3-mitmdump-爬取-“得到”-App-电子书信息"><a href="#11-3-mitmdump-爬取-“得到”-App-电子书信息" class="headerlink" title="11.3 mitmdump 爬取 “得到” App 电子书信息"></a>11.3 mitmdump 爬取 “得到” App 电子书信息</h1>
                  <p>“得到” App 是罗辑思维出品的一款碎片时间学习的 App，其官方网站为 <a href="https://www.igetget.com，App" target="_blank" rel="noopener">https://www.igetget.com，App</a> 内有很多学习资源。不过 “得到” App 没有对应的网页版，所以信息必须要通过 App 才可以获取。这次我们通过抓取其 App 来练习 mitmdump 的用法。</p>
                  <h3 id="1-爬取目标"><a href="#1-爬取目标" class="headerlink" title="1. 爬取目标"></a>1. 爬取目标</h3>
                  <p>我们的爬取目标是 App 内电子书版块的电子书信息，并将信息保存到 MongoDB，如图 11-30 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-031213.jpg" alt=""></p>
                  <p>我们要把图书的名称、简介、封面、价格爬取下来，不过这次爬取的侧重点还是了解 mitmdump 工具的用法，所以暂不涉及自动化爬取，App 的操作还是手动进行。mitmdump 负责捕捉响应并将数据提取保存。</p>
                  <h3 id="2-准备工作"><a href="#2-准备工作" class="headerlink" title="2. 准备工作"></a>2. 准备工作</h3>
                  <p>请确保已经正确安装好了 mitmproxy 和 mitmdump，手机和 PC 处于同一个局域网下，同时配置好了 mitmproxy 的 CA 证书，安装好 MongoDB 并运行其服务，安装 PyMongo 库，具体的配置可以参考第 1 章的说明。</p>
                  <h3 id="3-抓取分析"><a href="#3-抓取分析" class="headerlink" title="3. 抓取分析"></a>3. 抓取分析</h3>
                  <p>首先探寻一下当前页面的 URL 和返回内容，我们编写一个脚本如下所示：</p>
                  <figure class="highlight mel">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def response(<span class="keyword">flow</span>):</span><br><span class="line">    <span class="keyword">print</span>(<span class="keyword">flow</span>.request.url)</span><br><span class="line">    <span class="keyword">print</span>(<span class="keyword">flow</span>.response.<span class="keyword">text</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里只输出了请求的 URL 和响应的 Body 内容，也就是请求链接和响应内容这两个最关键的部分。脚本保存名称为 script.py。</p>
                  <p>接下来运行 mitmdump，命令如下所示：</p>
                  <figure class="highlight applescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">mitmdump -s <span class="keyword">script</span>.py</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>打开 “得到” App 的电子书页面，便可以看到 PC 端控制台有相应输出。接着滑动页面加载更多电子书，控制台新出现的输出内容就是 App 发出的新的加载请求，包含了下一页的电子书内容。控制台输出结果示例如图 11-31 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-031220.png" alt=""></p>
                  <p>图 11-31 控制台输出</p>
                  <p>可以看到 URL 为 <a href="https://dedao.igetget.com/v3/discover/bookList" target="_blank" rel="noopener">https://dedao.igetget.com/v3/discover/bookList</a> 的接口，其后面还加了一个 sign 参数。通过 URL 的名称，可以确定这就是获取电子书列表的接口。在 URL 的下方输出的是响应内容，是一个 JSON 格式的字符串，我们将它格式化，如图 11-32 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-031226.png" alt=""></p>
                  <p>图 11-32 格式化结果</p>
                  <p>格式化后的内容包含一个 c 字段、一个 list 字段，list 的每个元素都包含价格、标题、描述等内容。第一个返回结果是电子书《情人》，而此时 App 的内容也是这本电子书，描述的内容和价格也是完全匹配的，App 页面如图 11-33 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-031231.jpg" alt=""></p>
                  <p>图 11-33 APP 页面</p>
                  <p>这就说明当前接口就是获取电子书信息的接口，我们只需要从这个接口来获取内容就好了。然后解析返回结果，将结果保存到数据库。</p>
                  <h3 id="4-数据抓取"><a href="#4-数据抓取" class="headerlink" title="4. 数据抓取"></a>4. 数据抓取</h3>
                  <p>接下来我们需要对接口做过滤限制，抓取如上分析的接口，再提取结果中的对应字段。</p>
                  <p>这里，我们修改脚本如下所示：</p>
                  <figure class="highlight xl">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line">from mitmproxy <span class="keyword">import</span> ctx</span><br><span class="line"></span><br><span class="line">def response(flow):</span><br><span class="line">    url = <span class="string">'https://dedao.igetget.com/v3/discover/bookList'</span></span><br><span class="line">    <span class="keyword">if</span> flow.request.url.startswith(url):</span><br><span class="line">        <span class="keyword">text</span> = flow.response.<span class="keyword">text</span></span><br><span class="line">        <span class="keyword">data</span> = json.loads(<span class="keyword">text</span>)</span><br><span class="line">        books = <span class="keyword">data</span>.get(<span class="string">'c'</span>).get(<span class="string">'list'</span>)</span><br><span class="line">        <span class="keyword">for</span> book <span class="built_in">in</span> books:</span><br><span class="line">            ctx.<span class="built_in">log</span>.info(str(book))</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>重新滑动电子书页面，在 PC 端控制台观察输出，如图 11-34 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-031239.jpg" alt=""></p>
                  <p>图 11-34 控制台输出</p>
                  <p>现在输出了图书的全部信息，一本图书信息对应一条 JSON 格式的数据。</p>
                  <h3 id="5-提取保存"><a href="#5-提取保存" class="headerlink" title="5. 提取保存"></a>5. 提取保存</h3>
                  <p>接下来我们需要提取信息，再把信息保存到数据库中。方便起见，我们选择 MongoDB 数据库。</p>
                  <p>脚本还可以增加提取信息和保存信息的部分，修改代码如下所示：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">import json</span><br><span class="line">import pymongo</span><br><span class="line"><span class="keyword">from</span> mitmproxy import ctx</span><br><span class="line"></span><br><span class="line">client = pymongo.MongoClient(<span class="string">'localhost'</span>)</span><br><span class="line">db = client[<span class="string">'igetget'</span>]</span><br><span class="line">collection = db[<span class="string">'books'</span>]</span><br><span class="line"></span><br><span class="line">def response(flow):</span><br><span class="line">    global collection</span><br><span class="line">    url = <span class="string">'https://dedao.igetget.com/v3/discover/bookList'</span></span><br><span class="line">    <span class="keyword">if</span> flow.request.url.startswith(url):</span><br><span class="line">        text = flow.response.text</span><br><span class="line">        data = json.loads(text)</span><br><span class="line">        books = data.<span class="builtin-name">get</span>(<span class="string">'c'</span>).<span class="builtin-name">get</span>(<span class="string">'list'</span>)</span><br><span class="line">        <span class="keyword">for</span> book <span class="keyword">in</span> books:</span><br><span class="line">            data = &#123;<span class="string">'title'</span>: book.<span class="builtin-name">get</span>(<span class="string">'operating_title'</span>),</span><br><span class="line">                <span class="string">'cover'</span>: book.<span class="builtin-name">get</span>(<span class="string">'cover'</span>),</span><br><span class="line">                <span class="string">'summary'</span>: book.<span class="builtin-name">get</span>(<span class="string">'other_share_summary'</span>),</span><br><span class="line">                <span class="string">'price'</span>: book.<span class="builtin-name">get</span>(<span class="string">'price'</span>)</span><br><span class="line">            &#125;</span><br><span class="line">            ctx.log.<span class="builtin-name">info</span>(str(data))</span><br><span class="line">            collection.insert(data)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>重新滑动页面，控制台便会输出信息，如图 11-35 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-031246.jpg" alt=""></p>
                  <p>图 11-35 控制台输出</p>
                  <p>现在输出的每一条内容都是经过提取之后的内容，包含了电子书的标题、封面、描述、价格信息。</p>
                  <p>最开始我们声明了 MongoDB 的数据库连接，提取出信息之后调用该对象的 insert() 方法将数据插入到数据库即可。</p>
                  <p>滑动几页，发现所有图书信息都被保存到 MongoDB 中，如图 11-36 所示。</p>
                  <p><img src="https://qiniu.cuiqingcai.com/2019-11-27-031250.jpg" alt=""></p>
                  <p>目前为止，我们利用一个非常简单的脚本把 “得到” App 的电子书信息保存下来。</p>
                  <h3 id="6-本节代码"><a href="#6-本节代码" class="headerlink" title="6. 本节代码"></a>6. 本节代码</h3>
                  <p>本节的代码地址是：<a href="https://github.com/Python3WebSpider/IGetGet" target="_blank" rel="noopener"></a><a href="https://github.com/Python3WebSpider/IGetGet" target="_blank" rel="noopener">https://github.com/Python3WebSpider/IGetGet</a>。</p>
                  <h3 id="7-结语"><a href="#7-结语" class="headerlink" title="7. 结语"></a>7. 结语</h3>
                  <p>本节主要讲解了 mitmdump 的用法及脚本的编写方法。通过本节的实例，我们可以学习到如何实时将 App 的数据抓取下来。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-27 11:15:45" itemprop="dateCreated datePublished" datetime="2019-11-27T11:15:45+08:00">2019-11-27</time>
                </span>
                <span id="/8263.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 11.3-mitmdump 爬取 “得到” App 电子书信息" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>2.5k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>2 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8260.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8260.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 11.2-mitmproxy 的使用</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="11-2-mitmproxy-的使用"><a href="#11-2-mitmproxy-的使用" class="headerlink" title="11.2 mitmproxy 的使用"></a>11.2 mitmproxy 的使用</h1>
                  <p>mitmproxy 是一个支持 HTTP 和 HTTPS 的抓包程序，有类似 Fiddler、Charles 的功能，只不过它是一个控制台的形式操作。 mitmproxy 还有两个关联组件。一个是 mitmdump，它是 mitmproxy 的命令行接口，利用它我们可以对接 Python 脚本，用 Python 实现监听后的处理。另一个是 mitmweb，它是一个 Web 程序，通过它我们可以清楚观察 mitmproxy 捕获的请求。 下面我们来了解它们的用法。</p>
                  <h3 id="1-准备工作"><a href="#1-准备工作" class="headerlink" title="1. 准备工作"></a>1. 准备工作</h3>
                  <p>请确保已经正确安装好了 mitmproxy，并且手机和 PC 处于同一个局域网下，同时配置好了 mitmproxy 的 CA 证书，具体的配置可以参考第 1 章的说明。</p>
                  <h3 id="2-mitmproxy-的功能"><a href="#2-mitmproxy-的功能" class="headerlink" title="2. mitmproxy 的功能"></a>2. mitmproxy 的功能</h3>
                  <p>mitmproxy 有如下几项功能。</p>
                  <ul>
                    <li>拦截 HTTP 和 HTTPS 请求和响应</li>
                    <li>保存 HTTP 会话并进行分析</li>
                    <li>模拟客户端发起请求，模拟服务端返回响应</li>
                    <li>利用反向代理将流量转发给指定的服务器</li>
                    <li>支持 Mac 和 Linux 上的透明代理</li>
                    <li>利用 Python 对 HTTP 请求和响应进行实时处理</li>
                  </ul>
                  <h3 id="3-抓包原理"><a href="#3-抓包原理" class="headerlink" title="3. 抓包原理"></a>3. 抓包原理</h3>
                  <p>和 Charles 一样，mitmproxy 运行于自己的 PC 上，mitmproxy 会在 PC 的 8080 端口运行，然后开启一个代理服务，这个服务实际上是一个 HTTP/HTTPS 的代理。 手机和 PC 在同一个局域网内，设置代理为 mitmproxy 的代理地址，这样手机在访问互联网的时候流量数据包就会流经 mitmproxy，mitmproxy 再去转发这些数据包到真实的服务器，服务器返回数据包时再由 mitmproxy 转发回手机，这样 mitmproxy 就相当于起了中间人的作用，抓取到所有 Request 和 Response，另外这个过程还可以对接 mitmdump，抓取到的 Request 和 Response 的具体内容都可以直接用 Python 来处理，比如得到 Response 之后我们可以直接进行解析，然后存入数据库，这样就完成了数据的解析和存储过程。</p>
                  <h3 id="4-设置代理"><a href="#4-设置代理" class="headerlink" title="4. 设置代理"></a>4. 设置代理</h3>
                  <p>首先，我们需要运行 mitmproxy，命令如下所示： 启动 mitmproxy 的命令如下：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">mitmproxy</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行之后会在 8080 端口上运行一个代理服务，如图 11-12 所示： <img src="https://qiniu.cuiqingcai.com/2019-11-27-030652.jpg" alt=""> 图 11-12 mitmproxy 运行结果 右下角会出现当前正在监听的端口。 或者启动 mitmdump，它也会监听 8080 端口，命令如下所示：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">mitmdump</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果如图 11-13 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030657.jpg" alt=""> 图 11-13 MitmDump 运行结果 将手机和 PC 连接在同一局域网下，设置代理为当前代理。首先看看 PC 的当前局域网 IP。 Windows 上的命令如下所示：</p>
                  <figure class="highlight dos">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="built_in">ipconfig</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>Linux 和 Mac 上的命令如下所示：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">ifconfig</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>输出结果如图 11-14 所示： <img src="https://qiniu.cuiqingcai.com/2019-11-27-030701.jpg" alt=""> 图 11-14 查看局域网 IP 一般类似 10.<em>.</em>.<em> 或 172.16.</em>.<em> 或 192.168.1.</em> 这样的 IP 就是当前 PC 的局域网 IP，例如此图中 PC 的 IP 为 192.168.1.28，手机代理设置类似如图 11-15 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030706.jpg" alt=""> 图 11-15 代理设置 这样我们就配置好了 mitmproxy 的的代理。</p>
                  <h3 id="5-mitmproxy-的使用"><a href="#5-mitmproxy-的使用" class="headerlink" title="5. mitmproxy 的使用"></a>5. mitmproxy 的使用</h3>
                  <p>确保 mitmproxy 正常运行，并且手机和 PC 处于同一个局域网内，设置了 mitmproxy 的代理，具体的配置方法可以参考第 1 章。 运行 mitmproxy，命令如下所示：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">mitmproxy</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>设置成功之后，我们只需要在手机浏览器上访问任意的网页或浏览任意的 App 即可。例如在手机上打开百度，mitmproxy 页面便会呈现出手机上的所有请求，如图 11-16 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030716.jpg" alt=""> 图 11-16 所有请求 这就相当于之前我们在浏览器开发者工具监听到的浏览器请求，在这里我们借助于 mitmproxy 完成。Charles 完全也可以做到。 这里是刚才手机打开百度页面时的所有请求列表，左下角显示的 2/38 代表一共发生了 38 个请求，当前箭头所指的是第二个请求。 每个请求开头都有一个 GET 或 POST，这是各个请求的请求方式。紧接的是请求的 URL。第二行开头的数字就是请求对应的响应状态码，后面是响应内容的类型，如 text/html 代表网页文档、image/gif 代表图片。再往后是响应体的大小和响应的时间。 当前呈现了所有请求和响应的概览，我们可以通过这个页面观察到所有的请求。 如果想查看某个请求的详情，我们可以敲击回车，进入请求的详情页面，如图 11-17 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030720.png" alt=""> 图 11-17 详情页面 可以看到 Headers 的详细信息，如 Host、Cookies、User-Agent 等。 最上方是一个 Request、Response、Detail 的列表，当前处在 Request 这个选项上。这时我们再点击 TAB 键，即可查看这个请求对应的响应详情，如图 11-18 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030725.jpg" alt=""> 图 11-18 响应详情 最上面是响应头的信息，下拉之后我们可以看到响应体的信息。针对当前请求，响应体就是网页的源代码。 这时再敲击 TAB 键，切换到最后一个选项卡 Detail，即可看到当前请求的详细信息，如服务器的 IP 和端口、HTTP 协议版本、客户端的 IP 和端口等，如图 11-19 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030729.jpg" alt=""> 图 11-19 详细信息 mitmproxy 还提供了命令行式的编辑功能，我们可以在此页面中重新编辑请求。敲击 e 键即可进入编辑功能，这时它会询问你要编辑哪部分内容，如 Cookies、Query、URL 等，每个选项的第一个字母会高亮显示。敲击要编辑内容名称的首字母即可进入该内容的编辑页面，如敲击 m 即可编辑请求的方式，敲击 q 即可修改 GET 请求参数 Query。 这时我们敲击 q，进入到编辑 Query 的页面。由于没有任何参数，我们可以敲击 a 来增加一行，然后就可以输入参数对应的 Key 和 Value，如图 11-20 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030735.jpg" alt=""> 图 11-20 编辑页面 这里我们输入 Key 为 wd，Value 为 NBA。 然后再敲击 esc 键和 q 键，返回之前的页面，再敲击 e 和 p 键修改 Path。和上面一样，敲击 a 增加 Path 的内容，这时我们将 Path 修改为 s，如图 11-21 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030741.jpg" alt=""> 图 11-21 编辑页面 再敲击 esc 和 q 键返回，这时我们可以看到最上面的请求链接变成了 <a href="https://www.baidu.com/s?wd=NBA" target="_blank" rel="noopener">https://www.baidu.com/s?wd=NBA</a>，访问这个页面，可以看到百度搜索 NBA 关键词的搜索结果，如图 11-22 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030745.jpg" alt=""> 图 11-22 请求详情 敲击 a 保存修改，敲击 r 重新发起修改后的请求，即可看到上方请求方式前面多了一个回旋箭头，这说明重新执行了修改后的请求。这时我们再观察响应体内容，即可看到搜索 NBA 的页面结果的源代码，如图 11-23 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030750.jpg" alt=""> 图 11-23 响应结果 以上内容便是 mitmproxy 的简单用法。利用 mitmproxy，我们可以观察到手机上的所有请求，还可以对请求进行修改并重新发起。 Fiddler、Charles 也有这个功能，而且它们的图形界面操作更加方便。那么 mitmproxy 的优势何在？ mitmproxy 的强大之处体现在它的另一个工具 mitmdump，有了它我们可以直接对接 Python 对请求进行处理。下面我们来看看 mitmdump 的用法。</p>
                  <h3 id="6-MitmDump-的使用"><a href="#6-MitmDump-的使用" class="headerlink" title="6. MitmDump 的使用"></a>6. MitmDump 的使用</h3>
                  <p>mitmdump 是 mitmproxy 的命令行接口，同时还可以对接 Python 对请求进行处理，这是相比 Fiddler、Charles 等工具更加方便的地方。有了它我们可以不用手动截获和分析 HTTP 请求和响应，只需写好请求和响应的处理逻辑即可。它还可以实现数据的解析、存储等工作，这些过程都可以通过 Python 实现。</p>
                  <h4 id="实例引入"><a href="#实例引入" class="headerlink" title="实例引入"></a>实例引入</h4>
                  <p>我们可以使用命令启动 mitmproxy，并把截获的数据保存到文件中，命令如下所示：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">mitmdump -w outfile</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>其中 outfile 的名称任意，截获的数据都会被保存到此文件中。 还可以指定一个脚本来处理截获的数据，使用 - s 参数即可：</p>
                  <figure class="highlight applescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">mitmdump -s <span class="keyword">script</span>.py</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里指定了当前处理脚本为 script.py，它需要放置在当前命令执行的目录下。 我们可以在脚本里写入如下的代码：</p>
                  <figure class="highlight mel">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def request(<span class="keyword">flow</span>):</span><br><span class="line">    <span class="keyword">flow</span>.request.headers[<span class="string">'User-Agent'</span>] = <span class="string">'MitmProxy'</span></span><br><span class="line">    <span class="keyword">print</span>(<span class="keyword">flow</span>.request.headers)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们定义了一个 request() 方法，参数为 flow，它其实是一个 HTTPFlow 对象，通过 request 属性即可获取到当前请求对象。然后打印输出了请求的请求头，将请求头的 User-Agent 修改成了 MitmProxy。 运行之后我们在手机端访问 <a href="http://httpbin.org/get" target="_blank" rel="noopener">http://httpbin.org/get</a>，就可以看到有如下情况发生。 手机端的页面显示如图 11-24 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030755.jpg" alt=""> 图 11-24 手机端页面 PC 端控制台输出如图 11-25 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030800.png" alt=""> 图 11-25 PC 端控制台 手机端返回结果的 Headers 实际上就是请求的 Headers，User-Agent 被修改成了 mitmproxy。PC 端控制台输出了修改后的 Headers 内容，其 User-Agent 的内容正是 mitmproxy。 所以，通过这三行代码我们就可以完成对请求的改写。print() 方法输出结果可以呈现在 PC 端控制台上，可以方便地进行调试。</p>
                  <h4 id="日志输出"><a href="#日志输出" class="headerlink" title="日志输出"></a>日志输出</h4>
                  <p>mitmdump 提供了专门的日志输出功能，可以设定不同级别以不同颜色输出结果。我们把脚本修改成如下内容：</p>
                  <figure class="highlight mel">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">from mitmproxy import ctx</span><br><span class="line"></span><br><span class="line">def request(<span class="keyword">flow</span>):</span><br><span class="line">    <span class="keyword">flow</span>.request.headers[<span class="string">'User-Agent'</span>] = <span class="string">'MitmProxy'</span></span><br><span class="line">    ctx.<span class="keyword">log</span>.info(str(<span class="keyword">flow</span>.request.headers))</span><br><span class="line">    ctx.<span class="keyword">log</span>.warn(str(<span class="keyword">flow</span>.request.headers))</span><br><span class="line">    ctx.<span class="keyword">log</span>.<span class="keyword">error</span>(str(<span class="keyword">flow</span>.request.headers))</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里调用了 ctx 模块，它有一个 log 功能，调用不同的输出方法就可以输出不同颜色的结果，以方便我们做调试。例如，info() 方法输出的内容是白色的，warn() 方法输出的内容是黄色的，error() 方法输出的内容是红色的。运行结果如图 11-26 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030807.png" alt=""> 图 11-26 运行结果 不同的颜色对应不同级别的输出，我们可以将不同的结果合理划分级别输出，以更直观方便地查看调试信息。</p>
                  <h4 id="Request"><a href="#Request" class="headerlink" title="Request"></a>Request</h4>
                  <p>最开始我们实现了 request() 方法并且对 Headers 进行了修改。下面我们来看看 Request 还有哪些常用的功能。我们先用一个实例来感受一下。</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> mitmproxy import ctx</span><br><span class="line"></span><br><span class="line">def request(flow):</span><br><span class="line">    request = flow.request</span><br><span class="line">    <span class="builtin-name">info</span> = ctx.log.info</span><br><span class="line">    <span class="builtin-name">info</span>(request.url)</span><br><span class="line">    <span class="builtin-name">info</span>(str(request.headers))</span><br><span class="line">    <span class="builtin-name">info</span>(str(request.cookies))</span><br><span class="line">    <span class="builtin-name">info</span>(request.host)</span><br><span class="line">    <span class="builtin-name">info</span>(request.method)</span><br><span class="line">    <span class="builtin-name">info</span>(str(request.port))</span><br><span class="line">    <span class="builtin-name">info</span>(request.scheme)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们修改脚本，然后在手机上打开百度，即可看到 PC 端控制台输出了一系列的请求，在这里我们找到第一个请求。控制台打印输出了 Request 的一些常见属性，如 URL、Headers、Cookies、Host、Method、Scheme 等。输出结果如图 11-27 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030812.png" alt=""> 图 11-27 输出结果 结果中分别输出了请求链接、请求头、请求 Cookies、请求 Host、请求方法、请求端口、请求协议这些内容。 同时我们还可以对任意属性进行修改，就像最初修改 Headers 一样，直接赋值即可。例如，这里将请求的 URL 修改一下，脚本修改如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">request</span><span class="params">(flow)</span></span><span class="symbol">:</span></span><br><span class="line">    url = <span class="string">'https://httpbin.org/get'</span></span><br><span class="line">    flow.request.url = url</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>手机端得到如下结果，如图 11-28 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030817.jpg" alt=""> 图 11-28 手机端页面 比较有意思的是，浏览器最上方还是呈现百度的 URL，但是页面已经变成了 httpbin.org 的页面了。另外，Cookies 明显还是百度的 Cookies。我们只是用简单的脚本就成功把请求修改为其他的站点。通过这种方式修改和伪造请求就变得轻而易举。 通过这个实例我们知道，有时候 URL 虽然是正确的，但是内容并非是正确的。我们需要进一步提高自己的安全防范意识。 Request 还有很多属性，在此不再一一列举。更多属性可以参考：<a href="http://docs.mitmproxy.org/en/latest/scripting/api.html" target="_blank" rel="noopener">http://docs.mitmproxy.org/en/latest/scripting/api.html</a>。 只要我们了解了基本用法，会很容易地获取和修改 Reqeust 的任意内容，比如可以用修改 Cookies、添加代理等方式来规避反爬。</p>
                  <h4 id="Response"><a href="#Response" class="headerlink" title="Response"></a>Response</h4>
                  <p>对于爬虫来说，我们更加关心的其实是响应的内容，因为 Response Body 才是爬取的结果。对于响应来说，mitmdump 也提供了对应的处理接口，就是 response() 方法。下面我们用一个实例感受一下。</p>
                  <figure class="highlight isbl">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="variable">from</span> <span class="variable">mitmproxy</span> <span class="variable">import</span> <span class="variable">ctx</span></span><br><span class="line"></span><br><span class="line"><span class="variable">def</span> <span class="function"><span class="title">response</span>(<span class="variable">flow</span>):</span></span><br><span class="line"><span class="function">    <span class="variable">response</span> = <span class="variable">flow.response</span></span></span><br><span class="line"><span class="function">    <span class="variable">info</span> = <span class="variable">ctx.log.info</span></span></span><br><span class="line"><span class="function">    <span class="title">info</span>(<span class="title">str</span>(<span class="variable">response.status_code</span>))</span></span><br><span class="line">    <span class="function"><span class="title">info</span>(<span class="title">str</span>(<span class="variable">response.headers</span>))</span></span><br><span class="line">    <span class="function"><span class="title">info</span>(<span class="title">str</span>(<span class="variable">response.cookies</span>))</span></span><br><span class="line">    <span class="function"><span class="title">info</span>(<span class="title">str</span>(<span class="variable">response.text</span>))</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>将脚本修改为如上内容，然后手机访问：<a href="http://httpbin.org/get" target="_blank" rel="noopener">http://httpbin.org/get</a>。 这里打印输出了响应的 status_code、headers、cookies、text 这几个属性，其中最主要的 text 属性就是网页的源代码。 PC 端控制台输出如图 11-29 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-27-030826.png" alt=""> 图 11-29 PC 端控制台 控制台输出了响应的状态码、响应头、Cookies、响应体这几部分内容。 我们可以通过 response() 方法获取每个请求的响应内容。接下来再进行响应的信息提取和存储，我们就可以成功完成爬取了。</p>
                  <h3 id="7-结语"><a href="#7-结语" class="headerlink" title="7. 结语"></a>7. 结语</h3>
                  <p>本节介绍了 mitmproxy 和 mitmdump 的用法，在下一节我们会利用它们来实现一个 App 的爬取实战。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-27 11:11:52" itemprop="dateCreated datePublished" datetime="2019-11-27T11:11:52+08:00">2019-11-27</time>
                </span>
                <span id="/8260.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 11.2-mitmproxy 的使用" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>5.7k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>5 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8247.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8247.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 11.1-Charles 的使用</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="11-1-Charles-的使用"><a href="#11-1-Charles-的使用" class="headerlink" title="11.1 Charles 的使用"></a>11.1 Charles 的使用</h1>
                  <p>Charles 是一个网络抓包工具，我们可以用它来做 App 的抓包分析，得到 App 运行过程中发生的所有网络请求和响应内容，这就和 Web 端浏览器的开发者工具 Network 部分看到的结果一致。 相比 Fiddler 来说，Charles 的功能更强大，而且跨平台支持更好。所以我们选用 Charles 作为主要的移动端抓包工具，用于分析移动 App 的数据包，辅助完成 App 数据抓取工作。</p>
                  <h3 id="1-本节目标"><a href="#1-本节目标" class="headerlink" title="1. 本节目标"></a>1. 本节目标</h3>
                  <p>本节我们以京东 App 为例，通过 Charles 抓取 App 运行过程中的网络数据包，然后查看具体的 Request 和 Response 内容，以此来了解 Charles 的用法。</p>
                  <h3 id="2-准备工作"><a href="#2-准备工作" class="headerlink" title="2. 准备工作"></a>2. 准备工作</h3>
                  <p>请确保已经正确安装 Charles 并开启了代理服务，手机和 Charles 处于同一个局域网下，Charles 代理和 CharlesCA 证书设置好，另外需要开启 SSL 监听，具体的配置可以参考第 1 章的说明。</p>
                  <h3 id="3-原理"><a href="#3-原理" class="headerlink" title="3. 原理"></a>3. 原理</h3>
                  <p>首先 Charles 运行在自己的 PC 上，Charles 运行的时候会在 PC 的 8888 端口开启一个代理服务，这个服务实际上是一个 HTTP/HTTPS 的代理。 确保手机和 PC 在同一个局域网内，我们可以使用手机模拟器通过虚拟网络连接，也可以使用手机真机和 PC 通过无线网络连接。 设置手机代理为 Charles 的代理地址，这样手机访问互联网的数据包就会流经 Charles，Charles 再转发这些数据包到真实的服务器，服务器返回的数据包再由 Charles 转发回手机，Charles 就起到中间人的作用，所有流量包都可以捕捉到，因此所有 HTTP 请求和响应都可以捕获到。同时 Charles 还有权力对请求和响应进行修改。</p>
                  <h3 id="4-抓包"><a href="#4-抓包" class="headerlink" title="4. 抓包"></a>4. 抓包</h3>
                  <p>初始状态下 Charles 的运行界面如图 11-1 所示： <img src="https://qiniu.cuiqingcai.com/2019-11-26-034750.png" alt=""> 图 11-1 Charles 运行界面 Charles 会一直监听 PC 和手机发生的网络数据包，捕获到的数据包就会显示在左侧，随着时间的推移，捕获的数据包越来越多，左侧列表的内容也会越来越多。 可以看到，图中左侧显示了 Charles 抓取到的请求站点，我们点击任意一个条目便可以查看对应请求的详细信息，其中包括 Request、Response 等内容。 接下来清空 Charles 的抓取结果，点击左侧的扫帚按钮即可清空当前捕获到的所有请求。然后点击第二个监听按钮，确保监听按钮是打开的，这表示 Charles 正在监听 App 的网络数据流，如图 11-2 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-26-034803.png" alt=""> 图 11-2 监听过程 这时打开手机京东，注意一定要提前设置好 Charles 的代理并配置好 CA 证书，否则没有效果。 打开任意一个商品，如 iPhone，然后打开它的商品评论页面，如图 11-3 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-26-034824.png" alt=""> 图 11-3 评论页面 不断上拉加载评论，可以看到 Charles 捕获到这个过程中京东 App 内发生的所有网络请求，如图 11-4 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-26-034830.png" alt=""> 图 11-4 监听结果 左侧列表中会出现一个 api.m.jd.com 链接，而且它在不停闪动，很可能就是当前 App 发出的获取评论数据的请求被 Charles 捕获到了。我们点击将其展开，继续上拉刷新评论。随着上拉的进行，此处又会出现一个个网络请求记录，这时新出现的数据包请求确定就是获取评论的请求。 为了验证其正确性，我们点击查看其中一个条目的详情信息。切换到 Contents 选项卡，这时我们发现一些 JSON 数据，核对一下结果，结果有 commentData 字段，其内容和我们在 App 中看到的评论内容一致，如图 11-5 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-26-034834.png" alt=""> 图 11-5 Json 数据结果 这时可以确定，此请求对应的接口就是获取商品评论的接口。这样我们就成功捕获到了在上拉刷新的过程中发生的请求和响应内容。</p>
                  <h3 id="5-分析"><a href="#5-分析" class="headerlink" title="5. 分析"></a>5. 分析</h3>
                  <p>现在分析一下这个请求和响应的详细信息。首先可以回到 Overview 选项卡，上方显示了请求的接口 URL，接着是响应状态 Status Code、请求方式 Method 等，如图 11-6 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-26-034838.png" alt=""> 图 11-6 监听结果 这个结果和原本在 Web 端用浏览器开发者工具内捕获到的结果形式是类似的。 接下来点击 Contents 选项卡，查看该请求和响应的详情信息。 上半部分显示的是 Request 的信息，下半部分显示的是 Response 的信息。比如针对 Reqeust，我们切换到 Headers 选项卡即可看到该 Request 的 Headers 信息，针对 Response，我们切换到 JSON TEXT 选项卡即可看到该 Response 的 Body 信息，并且该内容已经被格式化，如图 11-7 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-26-034842.png" alt=""> 图 11-7 监听结果 由于这个请求是 POST 请求，所以我们还需要关心的就是 POST 的表单信息，切换到 Form 选项卡即可查看，如图 11-8 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-26-034848.png" alt=""> 图 11-8 监听结果 这样我们就成功抓取 App 中的评论接口的请求和响应，并且可以查看 Response 返回的 JSON 数据。 至于其他 App，我们同样可以使用这样的方式来分析。如果我们可以直接分析得到请求的 URL 和参数的规律，直接用程序模拟即可批量抓取。</p>
                  <h3 id="6-重发"><a href="#6-重发" class="headerlink" title="6. 重发"></a>6. 重发</h3>
                  <p>Charles 还有一个强大功能，它可以将捕获到的请求加以修改并发送修改后的请求。点击上方的修改按钮，左侧列表就多了一个以编辑图标为开头的链接，这就代表此链接对应的请求正在被我们修改，如图 11-9 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-26-034857.png" alt=""> 图 11-9 编辑页面 我们可以将 Form 中的某个字段移除，比如这里将 partner 字段移除，然后点击 Remove。这时我们已经对原来请求携带的 Form Data 做了修改，然后点击下方的 Execute 按钮即可执行修改后的请求，如图 11-10 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-26-034905.png" alt=""> 图 11-10 编辑页面 可以发现左侧列表再次出现了接口的请求结果，内容仍然不变，如图 11-11 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-26-034910.png" alt=""> 图 11-11 重新请求后结果 删除 Form 表单中的 partner 字段并没有带来什么影响，所以这个字段是无关紧要的。 有了这个功能，我们就可以方便地使用 Charles 来做调试，可以通过修改参数、接口等来测试不同请求的响应状态，就可以知道哪些参数是必要的哪些是不必要的，以及参数分别有什么规律，最后得到一个最简单的接口和参数形式以供程序模拟调用使用。</p>
                  <h3 id="7-结语"><a href="#7-结语" class="headerlink" title="7. 结语"></a>7. 结语</h3>
                  <p>以上内容便是通过 Charles 抓包分析 App 请求的过程。通过 Charles，我们成功抓取 App 中流经的网络数据包，捕获原始的数据，还可以修改原始请求和重新发起修改后的请求进行接口测试。 知道了请求和响应的具体信息，如果我们可以分析得到请求的 URL 和参数的规律，直接用程序模拟即可批量抓取，这当然最好不过了。 但是随着技术的发展，App 接口往往会带有密钥，我们并不能直接找到这些规律，那么怎么办呢？接下来，我们将了解利用 Charles 和 mitmdump 直接对接 Python 脚本实时处理抓取到的 Response 的过程。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-26 11:50:41" itemprop="dateCreated datePublished" datetime="2019-11-26T11:50:41+08:00">2019-11-26</time>
                </span>
                <span id="/8247.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 11.1-Charles 的使用" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>2.7k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>2 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8243.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8243.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 10.2-Cookies 池的搭建</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="10-2-Cookies-池的搭建"><a href="#10-2-Cookies-池的搭建" class="headerlink" title="10.2 Cookies 池的搭建"></a>10.2 Cookies 池的搭建</h1>
                  <p>很多时候，在爬取没有登录的情况下，我们也可以访问一部分页面或请求一些接口，因为毕竟网站本身需要做 SEO，不会对所有页面都设置登录限制。 但是，不登录直接爬取会有一些弊端，弊端主要有以下两点。</p>
                  <ul>
                    <li>设置了登录限制的页面无法爬取。如某论坛设置了登录才可查看资源，某博客设置了登录才可查看全文等，这些页面都需要登录账号才可以查看和爬取。</li>
                    <li>一些页面和接口虽然可以直接请求，但是请求一旦频繁，访问就容易被限制或者 IP 直接被封，但是登录之后就不会出现这样的问题，因此登录之后被反爬的可能性更低。</li>
                  </ul>
                  <p>下面我们就第二种情况做一个简单的实验。以微博为例，我们先找到一个 Ajax 接口，例如新浪财经官方微博的信息接口 <a href="https://m.weibo.cn/api/container/getIndex?uid=1638782947&amp;luicode=20000174" target="_blank" rel="noopener">https://m.weibo.cn/api/container/getIndex?uid=1638782947&amp;luicode=20000174</a> &amp;type=uid&amp;value=1638782947&amp;containerid=1005051638782947，如果用浏览器直接访问，返回的数据是 JSON 格式，如图 10-7 所示，其中包含了新浪财经官方微博的一些信息，直接解析 JSON 即可提取信息。 <img src="https://qiniu.cuiqingcai.com/2019-11-25-144100.png" alt=""> 图 10-7 返回数据 但是，这个接口在没有登录的情况下会有请求频率检测。如果一段时间内访问太过频繁，比如打开这个链接，一直不断刷新，则会看到请求频率过高的提示，如图 10-8 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-25-144106.png" alt=""> 图 10-8 提示页面 如果重新打开一个浏览器窗口，打开 <a href="https://passport.weibo.cn/signin/login?entry=mweibo&amp;r" target="_blank" rel="noopener">https://passport.weibo.cn/signin/login?entry=mweibo&amp;r</a>\= <a href="https://m.weibo.cn/，登录微博账号之后重新打开此链接，则页面正常显示接口的结果，而未登录的页面仍然显示请求过于频繁，如图" target="_blank" rel="noopener">https://m.weibo.cn/，登录微博账号之后重新打开此链接，则页面正常显示接口的结果，而未登录的页面仍然显示请求过于频繁，如图</a> 10-9 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-26-032830.jpg" alt=""> 图 10-9 对比页面 图中左侧是登录了账号之后请求接口的结果，右侧是未登录账号请求接口的结果，二者的接口链接是完全一样的。未登录状态无法正常访问，而登录状态可以正常显示。 因此，登录账号可以降低被封禁的概率。 我们可以尝试登录之后再做爬取，被封禁的几率会小很多，但是也不能完全排除被封禁的风险。如果一直用同一个账号频繁请求，那就有可能遇到请求过于频繁而封号的问题。 如果需要做大规模抓取，我们就需要拥有很多账号，每次请求随机选取一个账号，这样就降低了单个账号的访问频率，被封的概率又会大大降低。 那么如何维护多个账号的登录信息呢？这时就需要用到 Cookies 池了。接下来我们看看 Cookies 池的构建方法。</p>
                  <h3 id="1-本节目标"><a href="#1-本节目标" class="headerlink" title="1. 本节目标"></a>1. 本节目标</h3>
                  <p>我们以新浪微博为例来实现一个 Cookies 池的搭建过程。Cookies 池中保存了许多新浪微博账号和登录后的 Cookies 信息，并且 Cookies 池还需要定时检测每个 Cookies 的有效性，如果某 Cookies 无效，那就删除该 Cookies 并模拟登录生成新的 Cookies。同时 Cookies 池还需要一个非常重要的接口，即获取随机 Cookies 的接口，Cookies 运行后，我们只需请求该接口，即可随机获得一个 Cookies 并用其爬取。 由此可见，Cookies 池需要有自动生成 Cookies、定时检测 Cookies、提供随机 Cookies 等几大核心功能。</p>
                  <h3 id="2-准备工作"><a href="#2-准备工作" class="headerlink" title="2. 准备工作"></a>2. 准备工作</h3>
                  <p>搭建之前肯定需要一些微博的账号。需要安装好 Redis 数据库并使其正常运行。需要安装 Python 的 redis-py、requests、Selelnium 和 Flask 库。另外，还需要安装 Chrome 浏览器并配置好 ChromeDriver，其流程可以参考第一章的安装说明。</p>
                  <h3 id="3-Cookies-池架构"><a href="#3-Cookies-池架构" class="headerlink" title="3. Cookies 池架构"></a>3. Cookies 池架构</h3>
                  <p>Cookies 的架构和代理池类似，同样是 4 个核心模块，如图 10-10 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-25-144125.jpg" alt=""> 图 10-10 Cookies 池架构 Cookies 池架构的基本模块分为 4 块：存储模块、生成模块、检测模块和接口模块。每个模块的功能如下。</p>
                  <ul>
                    <li>存储模块负责存储每个账号的用户名密码以及每个账号对应的 Cookies 信息，同时还需要提供一些方法来实现方便的存取操作。</li>
                    <li>生成模块负责生成新的 Cookies。此模块会从存储模块逐个拿取账号的用户名和密码，然后模拟登录目标页面，判断登录成功，就将 Cookies 返回并交给存储模块存储。</li>
                    <li>检测模块需要定时检测数据库中的 Cookies。在这里我们需要设置一个检测链接，不同的站点检测链接不同，检测模块会逐个拿取账号对应的 Cookies 去请求链接，如果返回的状态是有效的，那么此 Cookies 没有失效，否则 Cookies 失效并移除。接下来等待生成模块重新生成即可。</li>
                    <li>接口模块需要用 API 来提供对外服务的接口。由于可用的 Cookies 可能有多个，我们可以随机返回 Cookies 的接口，这样保证每个 Cookies 都有可能被取到。Cookies 越多，每个 Cookies 被取到的概率就会越小，从而减少被封号的风险。</li>
                  </ul>
                  <p>以上设计 Cookies 池的基本思路和前面讲的代理池有相似之处。接下来我们设计整体的架构，然后用代码实现该 Cookies 池。</p>
                  <h3 id="4-Cookies-池的实现"><a href="#4-Cookies-池的实现" class="headerlink" title="4. Cookies 池的实现"></a>4. Cookies 池的实现</h3>
                  <p>首先分别了解各个模块的实现过程。</p>
                  <h4 id="存储模块"><a href="#存储模块" class="headerlink" title="存储模块"></a>存储模块</h4>
                  <p>其实，需要存储的内容无非就是账号信息和 Cookies 信息。账号由用户名和密码两部分组成，我们可以存成用户名和密码的映射。Cookies 可以存成 JSON 字符串，但是我们后面得需要根据账号来生成 Cookies。生成的时候我们需要知道哪些账号已经生成了 Cookies，哪些没有生成，所以需要同时保存该 Cookies 对应的用户名信息，其实也是用户名和 Cookies 的映射。这里就是两组映射，我们自然而然想到 Redis 的 Hash，于是就建立两个 Hash，结构分别如图 10-11 和图 10-12 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-25-144149.jpg" alt=""> 图 10-11 用户名密码 Hash 结构 <img src="https://qiniu.cuiqingcai.com/2019-11-25-144159.jpg" alt=""> 图 10-12 用户名 Cookies Hash 结构 Hash 的 Key 就是账号，Value 对应着密码或者 Cookies。另外需要注意，由于 Cookies 池需要做到可扩展，存储的账号和 Cookies 不一定单单只有本例中的微博，其他站点同样可以对接此 Cookies 池，所以这里 Hash 的名称可以做二级分类，例如存账号的 Hash 名称可以为 accounts:weibo，Cookies 的 Hash 名称可以为 cookies:weibo。如要扩展知乎的 Cookies 池，我们就可以使用 accounts:zhihu 和 cookies:zhihu，这样比较方便。 好，接下来我们就创建一个存储模块类，用以提供一些 Hash 的基本操作，代码如下：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">import</span> random</span><br><span class="line"><span class="keyword">import</span> redis</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">RedisClient</span><span class="params">(object)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, type, website, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        初始化 Redis 连接</span></span><br><span class="line"><span class="string">        :param host: 地址</span></span><br><span class="line"><span class="string">        :param port: 端口</span></span><br><span class="line"><span class="string">        :param password: 密码</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=<span class="literal">True</span>)</span><br><span class="line">        self.type = type</span><br><span class="line">        self.website = website</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">name</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        获取 Hash 的名称</span></span><br><span class="line"><span class="string">        :return: Hash 名称</span></span><br><span class="line"><span class="string">        """</span><span class="keyword">return</span><span class="string">"&#123;type&#125;:&#123;website&#125;"</span>.format(type=self.type, website=self.website)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">set</span><span class="params">(self, username, value)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        设置键值对</span></span><br><span class="line"><span class="string">        :param username: 用户名</span></span><br><span class="line"><span class="string">        :param value: 密码或 Cookies</span></span><br><span class="line"><span class="string">        :return:</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> self.db.hset(self.name(), username, value)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">get</span><span class="params">(self, username)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        根据键名获取键值</span></span><br><span class="line"><span class="string">        :param username: 用户名</span></span><br><span class="line"><span class="string">        :return:</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> self.db.hget(self.name(), username)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">delete</span><span class="params">(self, username)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        根据键名删除键值对</span></span><br><span class="line"><span class="string">        :param username: 用户名</span></span><br><span class="line"><span class="string">        :return: 删除结果</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> self.db.hdel(self.name(), username)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">count</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        获取数目</span></span><br><span class="line"><span class="string">        :return: 数目</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> self.db.hlen(self.name())</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">random</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        随机得到键值，用于随机 Cookies 获取</span></span><br><span class="line"><span class="string">        :return: 随机 Cookies</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> random.choice(self.db.hvals(self.name()))</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">usernames</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        获取所有账户信息</span></span><br><span class="line"><span class="string">        :return: 所有用户名</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> self.db.hkeys(self.name())</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">all</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        获取所有键值对</span></span><br><span class="line"><span class="string">        :return: 用户名和密码或 Cookies 的映射表</span></span><br><span class="line"><span class="string">        """</span><span class="keyword">return</span> self.db.hgetall(self.name())</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里我们新建了一个 RedisClient 类，初始化<strong>init</strong>() 方法有两个关键参数 type 和 website，分别代表类型和站点名称，它们就是用来拼接 Hash 名称的两个字段。如果这是存储账户的 Hash，那么此处的 type 为 accounts、website 为 weibo，如果是存储 Cookies 的 Hash，那么此处的 type 为 cookies、website 为 weibo。</p>
                  <p>接下来还有几个字段代表了 Redis 的连接信息，初始化时获得这些信息后初始化 StrictRedis 对象，建立 Redis 连接。</p>
                  <p>name() 方法拼接了 type 和 website，组成 Hash 的名称。set()、get()、delete() 方法分别代表设置、获取、删除 Hash 的某一个键值对，count() 获取 Hash 的长度。</p>
                  <p>比较重要的方法是 random()，它主要用于从 Hash 里随机选取一个 Cookies 并返回。每调用一次 random() 方法，就会获得随机的 Cookies，此方法与接口模块对接即可实现请求接口获取随机 Cookies。</p>
                  <h4 id="生成模块"><a href="#生成模块" class="headerlink" title="生成模块"></a>生成模块</h4>
                  <p>生成模块负责获取各个账号信息并模拟登录，随后生成 Cookies 并保存。我们首先获取两个 Hash 的信息，看看账户的 Hash 比 Cookies 的 Hash 多了哪些还没有生成 Cookies 的账号，然后将剩余的账号遍历，再去生成 Cookies 即可。</p>
                  <p>这里主要逻辑就是找出那些还没有对应 Cookies 的账号，然后再逐个获取 Cookies，代码如下：</p>
                  <p>​```python<br>for username in accounts_usernames:<br> if not username in cookies_usernames:<br> password = self.accounts_db.get(username)<br> print(‘ 正在生成 Cookies’, ‘ 账号 ‘, username, ‘ 密码 ‘, password)<br> result = self.new_cookies(username, password)<br>
                  <figure class="highlight mathematica">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"></span><br><span class="line">因为我们对接的是新浪微博，前面我们已经破解了新浪微博的四宫格验证码，在这里我们直接对接过来即可，不过现在需要加一个获取 <span class="keyword">Cookies</span> 的方法，并针对不同的情况返回不同的结果，逻辑如下所示：</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure><br>def get_cookies(self):<br> return self.browser.get_cookies()</p>
                  <p>def main(self):<br> self.open()<br> if self.password_error():<br> return {<br> ‘status’: 2,<br> ‘content’: ‘ 用户名或密码错误 ‘<br> }</p>
                  <pre><code># 如果不需要验证码直接登录成功
if self.login_successfully():
    cookies = self.get_cookies()
    return {
        &#39;status&#39;: 1,
        &#39;content&#39;: cookies
    }
# 获取验证码图片
image = self.get_image(&#39;captcha.png&#39;)
numbers = self.detect_image(image)
self.move(numbers)
if self.login_successfully():
    cookies = self.get_cookies()
    return {
        &#39;status&#39;: 1,
        &#39;content&#39;: cookies
    }
else:
    return {
        &#39;status&#39;: 3,
        &#39;content&#39;: &#39; 登录失败 &#39;
    }
</code></pre>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"></span><br><span class="line">这里返回结果的类型是字典，并且附有状态码 status，在生成模块里我们可以根据不同的状态码做不同的处理。例如状态码为 <span class="number">1</span> 的情况，表示成功获取 Cookies，我们只需要将 Cookies 保存到数据库即可。如状态码为 <span class="number">2</span> 的情况，代表用户名或密码错误，那么我们就应该把当前数据库中存储的账号信息删除。如状态码为 <span class="number">3</span> 的情况，则代表登录失败的一些错误，此时不能判断是否用户名或密码错误，也不能成功获取 Cookies，那么简单提示再进行下一个处理即可，类似代码实现如下所示：</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>result = self.new_cookies(username, password)</p>
                  <h1 id="成功获取"><a href="#成功获取" class="headerlink" title="成功获取"></a>成功获取</h1>
                  <p>if result.get(‘status’) == 1:<br> cookies = self.process_cookies(result.get(‘content’))<br> print(‘ 成功获取到 Cookies’, cookies)<br> if self.cookies_db.set(username, json.dumps(cookies)):<br> print(‘ 成功保存 Cookies’)</p>
                  <h1 id="密码错误，移除账号"><a href="#密码错误，移除账号" class="headerlink" title="密码错误，移除账号"></a>密码错误，移除账号</h1>
                  <p>elif result.get(‘status’) == 2:<br> print(result.get(‘content’))<br> if self.accounts<em>db.delete(username):<br> print(‘ 成功删除账号 ‘)<br>else:<br> print(result.get(‘content’))<br>
                      <figure class="highlight mathematica">
                        <table>
                          <tr>
                            <td class="gutter">
                              <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                            </td>
                            <td class="code">
                              <pre><span class="line"></span><br><span class="line">如果要扩展其他站点，只需要实现 new_cookies() 方法即可，然后按此处理规则返回对应的模拟登录结果，比如 <span class="number">1</span> 代表获取成功，<span class="number">2</span> 代表用户名或密码错误。 代码运行之后就会遍历一次尚未生成 <span class="keyword">Cookies</span> 的账号，模拟登录生成新的 <span class="keyword">Cookies</span>。</span><br><span class="line"></span><br><span class="line">#### 检测模块</span><br><span class="line"></span><br><span class="line">我们现在可以用生成模块来生成 <span class="keyword">Cookies</span>，但还是免不了 <span class="keyword">Cookies</span> 失效的问题，例如时间太长导致 <span class="keyword">Cookies</span> 失效，或者 <span class="keyword">Cookies</span> 使用太频繁导致无法正常请求网页。如果遇到这样的 <span class="keyword">Cookies</span>，我们肯定不能让它继续保存在数据库里。 所以我们还需要增加一个定时检测模块，它负责遍历池中的所有 <span class="keyword">Cookies</span>，同时设置好对应的检测链接，我们用一个个 <span class="keyword">Cookies</span> 去请求这个链接。如果请求成功，或者状态码合法，那么该 <span class="keyword">Cookies</span> 有效；如果请求失败，或者无法获取正常的数据，比如直接跳回登录页面或者跳到验证页面，那么此 <span class="keyword">Cookies</span> 无效，我们需要将该 <span class="keyword">Cookies</span> 从数据库中移除。 此 <span class="keyword">Cookies</span> 移除之后，刚才所说的生成模块就会检测到 <span class="keyword">Cookies</span> 的 <span class="keyword">Hash</span> 和账号的 <span class="keyword">Hash</span> 相比少了此账号的 <span class="keyword">Cookies</span>，生成模块就会认为这个账号还没生成 <span class="keyword">Cookies</span>，那么就会用此账号重新登录，此账号的 <span class="keyword">Cookies</span> 又被重新更新。 检测模块需要做的就是检测 <span class="keyword">Cookies</span> 失效，然后将其从数据中移除。 为了实现通用可扩展性，我们首先定义一个检测器的父类，声明一些通用组件，实现如下所示：</span><br></pre>
                            </td>
                          </tr>
                        </table>
                      </figure><br>class ValidTester(object):<br> def <em>_init</em>
                    </em>(self, website=’default’):<br> self.website = website<br> self.cookies_db = RedisClient(‘cookies’, self.website)<br> self.accounts_db = RedisClient(‘accounts’, self.website)</p>
                  <pre><code>def test(self, username, cookies):
    raise NotImplementedError

def run(self):
    cookies_groups = self.cookies_db.all()
    for username, cookies in cookies_groups.items():
        self.test(username, cookies)
</code></pre>
                  <figure class="highlight dockerfile">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"></span><br><span class="line">在这里定义了一个父类叫作 ValidTester，在**init**() 方法里指定好站点的名称 website，另外建立两个存储模块连接对象 cookies_db 和 accounts_db，分别负责操作 Cookies 和账号的 Hash，<span class="keyword">run</span><span class="bash">() 方法是入口，在这里是遍历了所有的 Cookies，然后调用 <span class="built_in">test</span>() 方法进行测试，在这里 <span class="built_in">test</span>() 方法是没有实现的，也就是说我们需要写一个子类来重写这个 <span class="built_in">test</span>() 方法，每个子类负责各自不同网站的检测，如检测微博的就可以定义为 WeiboValidTester，实现其独有的 <span class="built_in">test</span>() 方法来检测微博的 Cookies 是否合法，然后做相应的处理，所以在这里我们还需要再加一个子类来继承这个 ValidTester，重写其 <span class="built_in">test</span>() 方法，实现如下：</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>import json<br>import requests<br>from requests.exceptions import ConnectionError</p>
                  <p>class WeiboValidTester(ValidTester):<br> def <strong>init</strong>(self, website=’weibo’):<br> ValidTester.<strong>init</strong>(self, website)</p>
                  <pre><code>def test(self, username, cookies):
    print(&#39; 正在测试 Cookies&#39;, &#39; 用户名 &#39;, username)
    try:
        cookies = json.loads(cookies)
    except TypeError:
        print(&#39;Cookies 不合法 &#39;, username)
        self.cookies_db.delete(username)
        print(&#39; 删除 Cookies&#39;, username)
        return
    try:
        test_url = TEST_URL_MAP[self.website]
        response = requests.get(test_url, cookies=cookies, timeout=5, allow_redirects=False)
        if response.status_code == 200:
            print(&#39;Cookies 有效 &#39;, username)
            print(&#39; 部分测试结果 &#39;, response.text[0:50])
        else:
            print(response.status_code, response.headers)
            print(&#39;Cookies 失效 &#39;, username)
            self.cookies_db.delete(username)
            print(&#39; 删除 Cookies&#39;, username)
    except ConnectionError as e:
        print(&#39; 发生异常 &#39;, e.args)
</code></pre>
                  <figure class="highlight mathematica">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"></span><br><span class="line">test() 方法首先将 <span class="keyword">Cookies</span> 转化为字典，检测 <span class="keyword">Cookies</span> 的格式，如果格式不正确，直接将其删除，如果格式没问题，那么就拿此 <span class="keyword">Cookies</span> 请求被检测的 <span class="keyword">URL</span>。test() 方法在这里检测微博，检测的 <span class="keyword">URL</span> 可以是某个 Ajax 接口，为了实现可配置化，我们将测试 <span class="keyword">URL</span> 也定义成字典，如下所示：</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>TEST<em>URL<em>MAP = {‘weibo’: ‘<a href="https://m.weibo.cn/&#39;}" target="_blank" rel="noopener">https://m.weibo.cn/&#39;}</a><br>
                        <figure class="highlight mathematica">
                          <table>
                            <tr>
                              <td class="gutter">
                                <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                              </td>
                              <td class="code">
                                <pre><span class="line"></span><br><span class="line">如果要扩展其他站点，我们可以统一在字典里添加。对微博来说，我们用 <span class="keyword">Cookies</span> 去请求目标站点，同时禁止重定向和设置超时时间，得到响应之后检测其返回状态码。如果直接返回 <span class="number">200</span> 状态码，则 <span class="keyword">Cookies</span> 有效，否则可能遇到了 <span class="number">302</span> 跳转等情况，一般会跳转到登录页面，则 <span class="keyword">Cookies</span> 已失效。如果 <span class="keyword">Cookies</span> 失效，我们将其从 <span class="keyword">Cookies</span> 的 <span class="keyword">Hash</span> 里移除即可。</span><br><span class="line"></span><br><span class="line">#### 接口模块</span><br><span class="line"></span><br><span class="line">生成模块和检测模块如果定时运行就可以完成 <span class="keyword">Cookies</span> 实时检测和更新。但是 <span class="keyword">Cookies</span> 最终还是需要给爬虫来用，同时一个 <span class="keyword">Cookies</span> 池可供多个爬虫使用，所以我们还需要定义一个 Web 接口，爬虫访问此接口便可以取到随机的 <span class="keyword">Cookies</span>。我们采用 Flask 来实现接口的搭建，代码如下所示：</span><br></pre>
                              </td>
                            </tr>
                          </table>
                        </figure><br>import json<br>from flask import Flask, g<br>app = Flask(__name
                      </em></em>)</p>
                  <h1 id="生成模块的配置字典"><a href="#生成模块的配置字典" class="headerlink" title="生成模块的配置字典"></a>生成模块的配置字典</h1>
                  <p>GENERATOR_MAP = {‘weibo’: ‘WeiboCookiesGenerator’}<br>@app.route(‘/‘)<br>def index():<br> return ‘
                  <h2>Welcome to Cookie Pool System</h2>‘</p>
                  <p>def get_conn():<br> for website in GENERATOR_MAP:<br> if not hasattr(g, website):<br> setattr(g, website + ‘_cookies’, eval(‘RedisClient’ + ‘(“cookies”, “‘ + website + ‘“)’))<br> return g</p>
                  <p>@app.route(‘/<website>/random’)<br>def random(website):<br> “””<br> 获取随机的 Cookie, 访问地址如 /weibo/random<br> :return: 随机 Cookie<br> “””<br> g = get_conn()<br> cookies = getattr(g, website + ‘_cookies’).random()<br> return cookies<br>
                      <figure class="highlight clean">
                        <table>
                          <tr>
                            <td class="gutter">
                              <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                            </td>
                            <td class="code">
                              <pre><span class="line"></span><br><span class="line">我们同样需要实现通用的配置来对接不同的站点，所以接口链接的第一个字段定义为站点名称，第二个字段定义为获取的方法，例如，/weibo/random 是获取微博的随机 Cookies，/zhihu/random 是获取知乎的随机 Cookies。</span><br><span class="line"></span><br><span class="line">#### 调度模块</span><br><span class="line"></span><br><span class="line">最后，我们再加一个调度模块让这几个模块配合运行起来，主要的工作就是驱动几个模块定时运行，同时各个模块需要在不同进程上运行，实现如下所示：</span><br></pre>
                            </td>
                          </tr>
                        </table>
                      </figure><br>import time<br>from multiprocessing import Process<br>from cookiespool.api import app<br>from cookiespool.config import <em><br>from cookiespool.generator import </em><br>from cookiespool.tester import *</p>
                  <p>class Scheduler(object):<br> @staticmethod<br> def valid_cookie(cycle=CYCLE):<br> while True:<br> print(‘Cookies 检测进程开始运行 ‘)<br> try:<br> for website, cls in TESTER_MAP.items():<br> tester = eval(cls + ‘(website=”‘ + website + ‘“)’)<br> tester.run()<br> print(‘Cookies 检测完成 ‘)<br> del tester<br> time.sleep(cycle)<br> except Exception as e:<br> print(e.args)</p>
                  <pre><code>@staticmethod
def generate_cookie(cycle=CYCLE):
    while True:
        print(&#39;Cookies 生成进程开始运行 &#39;)
        try:
            for website, cls in GENERATOR_MAP.items():
                generator = eval(cls + &#39;(website=&quot;&#39; + website + &#39;&quot;)&#39;)
                generator.run()
                print(&#39;Cookies 生成完成 &#39;)
                generator.close()
                time.sleep(cycle)
        except Exception as e:
            print(e.args)

@staticmethod
def api():
    print(&#39;API 接口开始运行 &#39;)
    app.run(host=API_HOST, port=API_PORT)

def run(self):
    if API_PROCESS:
        api_process = Process(target=Scheduler.api)
        api_process.start()

    if GENERATOR_PROCESS:
        generate_process = Process(target=Scheduler.generate_cookie)
        generate_process.start()

    if VALID_PROCESS:
        valid_process = Process(target=Scheduler.valid_cookie)
        valid_process.start()
</code></pre>
                  <figure class="highlight plain">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"></span><br><span class="line">这里用到了两个重要的配置，即产生模块类和测试模块类的字典配置，如下所示：</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h1 id="产生模块类，如扩展其他站点，请在此配置"><a href="#产生模块类，如扩展其他站点，请在此配置" class="headerlink" title="产生模块类，如扩展其他站点，请在此配置"></a>产生模块类，如扩展其他站点，请在此配置</h1>
                  <p>GENERATOR_MAP = {‘weibo’: ‘WeiboCookiesGenerator’}</p>
                  <h1 id="测试模块类，如扩展其他站点，请在此配置"><a href="#测试模块类，如扩展其他站点，请在此配置" class="headerlink" title="测试模块类，如扩展其他站点，请在此配置"></a>测试模块类，如扩展其他站点，请在此配置</h1>
                  <p>TESTER_MAP = {‘weibo’: ‘WeiboValidTester’}<br>
                  <figure class="highlight plain">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"></span><br><span class="line">这样的配置是为了方便动态扩展使用的，键名为站点名称，键值为类名。如需要配置其他站点可以在字典中添加，如扩展知乎站点的产生模块，则可以配置成：</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure><br>GENERATOR_MAP = {<br> ‘weibo’: ‘WeiboCookiesGenerator’,<br> ‘zhihu’: ‘ZhihuCookiesGenerator’,<br>}<br>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"></span><br><span class="line">Scheduler 里将字典进行遍历，同时利用 eval() 动态新建各个类的对象，调用其入口 <span class="builtin-name">run</span>() 方法运行各个模块。同时，各个模块的多进程使用了 multiprocessing 中的 Process 类，调用其 start() 方法即可启动各个进程。 另外，各个模块还设有模块开关，我们可以在配置文件中自由设置开关的开启和关闭，如下所示：</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  </p>
                  <h1 id="产生模块开关"><a href="#产生模块开关" class="headerlink" title="产生模块开关"></a>产生模块开关</h1>
                  <p>GENERATOR_PROCESS = True</p>
                  <h1 id="验证模块开关"><a href="#验证模块开关" class="headerlink" title="验证模块开关"></a>验证模块开关</h1>
                  <p>VALID_PROCESS = False</p>
                  <h1 id="接口模块开关"><a href="#接口模块开关" class="headerlink" title="接口模块开关"></a>接口模块开关</h1>
                  <p>API_PROCESS = True<br>
                  <figure class="highlight yaml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"></span><br><span class="line"><span class="string">定义为</span> <span class="literal">True</span> <span class="string">即可开启该模块，定义为</span> <span class="literal">False</span> <span class="string">即关闭此模块。</span> <span class="string">至此，我们的</span> <span class="string">Cookies</span> <span class="string">就全部完成了。接下来我们将模块同时开启，启动调度器，控制台类似输出如下所示：</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure><br>API 接口开始运行</p>
                  <ul>
                    <li>Running on <a href="http://0.0.0.0:5000/" target="_blank" rel="noopener">http://0.0.0.0:5000/</a> (Press CTRL+C to quit)<br>Cookies 生成进程开始运行<br>Cookies 检测进程开始运行<br>正在生成 Cookies 账号 14747223314 密码 asdf1129<br>正在测试 Cookies 用户名 14747219309<br>Cookies 有效 14747219309<br>正在测试 Cookies 用户名 14740626332<br>Cookies 有效 14740626332<br>正在测试 Cookies 用户名 14740691419<br>Cookies 有效 14740691419<br>正在测试 Cookies 用户名 14740618009<br>Cookies 有效 14740618009<br>正在测试 Cookies 用户名 14740636046<br>Cookies 有效 14740636046<br>正在测试 Cookies 用户名 14747222472<br>Cookies 有效 14747222472<br>Cookies 检测完成<br>验证码位置 420 580 384 544<br>成功匹配<br>拖动顺序 [1, 4, 2, 3]<br>成功获取到 Cookies {‘SUHB’: ‘08J77UIj4w5n_T’, ‘SCF’: ‘AimcUCUVvHjswSBmTswKh0g4kNj4K7_U9k57YzxbqFt4SFBhXq3Lx4YSNO9VuBV841BMHFIaH4ipnfqZnK7W6Qs.’, ‘SSOLoginState’: ‘1501439488’, ‘_T_WM’: ‘99b7d656220aeb9207b5db97743adc02’, ‘M_WEIBOCN_PARAMS’: ‘uicode%3D20000174’, ‘SUB’: ‘_2A250elZQDeRhGeBM6VAR8ifEzTuIHXVXhXoYrDV6PUJbkdBeLXTxkW17ZoYhhJ92N_RGCjmHpfv9TB8OJQ..’}<br>成功保存 Cookies<br>```</li>
                  </ul>
                  <p>以上所示是程序运行的控制台输出内容，我们从中可以看到各个模块都正常启动，测试模块逐个测试 Cookies，生成模块获取尚未生成 Cookies 的账号的 Cookies，各个模块并行运行，互不干扰。 我们可以访问接口获取随机的 Cookies，如图 10-13 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-25-144223.jpg" alt=""> 图 10-13 接口页面 爬虫只需要请求该接口就可以实现随机 Cookies 的获取。</p>
                  <h3 id="5-本节代码"><a href="#5-本节代码" class="headerlink" title="5. 本节代码"></a>5. 本节代码</h3>
                  <p>本节代码地址：<a href="https://github.com/Python3WebSpider/CookiesPool" target="_blank" rel="noopener">https://github.com/Python3WebSpider/CookiesPool</a>。</p>
                  <h3 id="6-结语"><a href="#6-结语" class="headerlink" title="6. 结语"></a>6. 结语</h3>
                  <p>以上内容便是 Cookies 池的用法，后文中我们会利用该 Cookies 池和之前所讲的代理池来进行新浪微博的大规模爬取。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-26 11:44:50" itemprop="dateCreated datePublished" datetime="2019-11-26T11:44:50+08:00">2019-11-26</time>
                </span>
                <span id="/8243.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 10.2-Cookies 池的搭建" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>12k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>11 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8229.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8229.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 10.1-模拟登录并爬取 GitHub</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>我们先以一个最简单的实例来了解模拟登录后页面的抓取过程，其原理在于模拟登录后 Cookies 的维护。</p>
                  <h3 id="1-本节目标"><a href="#1-本节目标" class="headerlink" title="1. 本节目标"></a>1. 本节目标</h3>
                  <p>本节将讲解以 GitHub 为例来实现模拟登录的过程，同时爬取登录后才可以访问的页面信息，如好友动态、个人信息等内容。 我们应该都听说过 GitHub，如果在我们在 Github 上关注了某些人，在登录之后就会看到他们最近的动态信息，比如他们最近收藏了哪个 Repository，创建了哪个组织，推送了哪些代码。但是退出登录之后，我们就无法再看到这些信息。 如果希望爬取 GitHub 上所关注人的最近动态，我们就需要模拟登录 GitHub。</p>
                  <h3 id="2-环境准备"><a href="#2-环境准备" class="headerlink" title="2. 环境准备"></a>2. 环境准备</h3>
                  <p>请确保已经安装好了 requests 和 lxml 库，如没有安装可以参考第 1 章的安装说明。</p>
                  <h3 id="3-分析登录过程"><a href="#3-分析登录过程" class="headerlink" title="3. 分析登录过程"></a>3. 分析登录过程</h3>
                  <p>首先要分析登录的过程，需要探究后台的登录请求是怎样发送的，登录之后又有怎样的处理过程。 如果已经登录 GitHub，先退出登录，同时清除 Cookies。 打开 GitHub 的登录页面，链接为 <a href="https://github.com/login，输入" target="_blank" rel="noopener">https://github.com/login，输入</a> GitHub 的用户名和密码，打开开发者工具，将 Preserve Log 选项勾选上，这表示显示持续日志，如图 10-1 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-24-153921.png" alt=""> 图 10-1 开发者工具设置 点击登录按钮，这时便会看到开发者工具下方显示了各个请求过程，如图 10-2 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-24-154042.jpg" alt=""> 图 10-2 请求过程 点击第一个请求，进入其详情页面，如图 10-3 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-24-154052.jpg" alt=""> 图 10-3 详情页面 可以看到请求的 URL 为 <a href="https://github.com/session，请求方式为" target="_blank" rel="noopener">https://github.com/session，请求方式为</a> POST。再往下看，我们观察到它的 Form Data 和 Headers 这两部分内容，如图 10-4 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-24-154058.jpg" alt=""> 图 10-4 详情页面 Headers 里面包含了 Cookies、Host、Origin、Referer、User-Agent 等信息。Form Data 包含了 5 个字段，commit 是固定的字符串 Sign in，utf8 是一个勾选字符，authenticity_token 较长，其初步判断是一个 Base64 加密的字符串，login 是登录的用户名，password 是登录的密码。 综上所述，我们现在无法直接构造的内容有 Cookies 和 authenticity_token。下面我们再来探寻一下这两部分内容如何获取。 在登录之前我们会访问到一个登录页面，此页面是通过 GET 形式访问的。输入用户名密码，点击登录按钮，浏览器发送这两部分信息，也就是说 Cookies 和 authenticity_token 一定是在访问登录页的时候设置的。 这时再退出登录，回到登录页，同时清空 Cookies，重新访问登录页，截获发生的请求，如图 10-5 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-24-154127.jpg" alt=""> 图 10-5 截获请求 访问登录页面的请求如图所示，Response Headers 有一个 Set-Cookie 字段。这就是设置 Cookies 的过程。 另外，我们发现 Response Headers 没有和 authenticity_token 相关的信息，所以可能 authenticity_token 还隐藏在其他的地方或者是计算出来的。我们再从网页的源码探寻，搜索相关字段，发现源代码里面隐藏着此信息，它是一个隐藏式表单元素，如图 10-6 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-24-154124.jpg" alt=""> 图 10-6 表单元素 现在我们已经获取到所有信息，接下来实现模拟登录。</p>
                  <h3 id="4-代码实战"><a href="#4-代码实战" class="headerlink" title="4. 代码实战"></a>4. 代码实战</h3>
                  <p>首先我们定义一个 Login 类，初始化一些变量：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Login</span>(<span class="title">object</span>):</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">        <span class="keyword">self</span>.headers = &#123;</span><br><span class="line">            <span class="string">'Referer'</span>: <span class="string">'https://github.com/'</span>,</span><br><span class="line">            <span class="string">'User-Agent'</span>: <span class="string">'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36'</span>,</span><br><span class="line">            <span class="string">'Host'</span>: <span class="string">'github.com'</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">self</span>.login_url = <span class="string">'https://github.com/login'</span></span><br><span class="line">        <span class="keyword">self</span>.post_url = <span class="string">'https://github.com/session'</span></span><br><span class="line">        <span class="keyword">self</span>.logined_url = <span class="string">'https://github.com/settings/profile'</span></span><br><span class="line">        <span class="keyword">self</span>.session = requests.Session()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里最重要的一个变量就是 requests 库的 Session，它可以帮助我们维持一个会话，而且可以自动处理 Cookies，我们不用再去担心 Cookies 的问题。 接下来，访问登录页面要完成两件事：一是通过此页面获取初始的 Cookies，二是提取出 authenticity_token。 在这里我们实现一个 token() 方法，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">from lxml import etree</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">token</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">    response = <span class="keyword">self</span>.session.get(<span class="keyword">self</span>.login_url, headers=<span class="keyword">self</span>.headers)</span><br><span class="line">    selector = etree.HTML(response.text)</span><br><span class="line">    token = selector.xpath(<span class="string">'//div/input[2]/@value'</span>)[<span class="number">0</span>]</span><br><span class="line">    <span class="keyword">return</span> token</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们用 Session 对象的 get() 方法访问 GitHub 的登录页面，然后用 XPath 解析出登录所需的 authenticity_token 信息并返回。 现在已经获取初始的 Cookies 和 authenticity_token，开始模拟登录，实现一个 login() 方法，如下所示：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">login</span><span class="params">(<span class="keyword">self</span>, email, password)</span></span><span class="symbol">:</span></span><br><span class="line">    post_data = &#123;</span><br><span class="line">        <span class="string">'commit'</span>: <span class="string">'Sign in'</span>,</span><br><span class="line">        <span class="string">'utf8'</span>: <span class="string">'✓'</span>,</span><br><span class="line">        <span class="string">'authenticity_token'</span>: <span class="keyword">self</span>.token(),</span><br><span class="line">        <span class="string">'login'</span>: email,</span><br><span class="line">        <span class="string">'password'</span>: password</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    response = <span class="keyword">self</span>.session.post(<span class="keyword">self</span>.post_url, data=post_data, headers=<span class="keyword">self</span>.headers)</span><br><span class="line">    <span class="keyword">if</span> response.status_code == <span class="number">200</span><span class="symbol">:</span></span><br><span class="line">        <span class="keyword">self</span>.dynamics(response.text)</span><br><span class="line"></span><br><span class="line">    response = <span class="keyword">self</span>.session.get(<span class="keyword">self</span>.logined_url, headers=<span class="keyword">self</span>.headers)</span><br><span class="line">    <span class="keyword">if</span> response.status_code == <span class="number">200</span><span class="symbol">:</span></span><br><span class="line">        <span class="keyword">self</span>.profile(response.text)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>首先构造一个表单，复制各个字段，其中 email 和 password 是以变量的形式传递。然后再用 Session 对象的 post() 方法模拟登录即可。由于 requests 自动处理了重定向信息，我们登录成功后就可以直接跳转到首页，首页会显示所关注人的动态信息，得到响应之后我们用 dynamics() 方法来对其进行处理。接下来再用 Session 对象请求个人详情页，然后用 profile() 方法来处理个人详情页信息。 其中，dynamics() 方法和 profile() 方法的实现如下所示：</p>
                  <figure class="highlight julia">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def dynamics(self, html):</span><br><span class="line">    selector = etree.<span class="built_in">HTML</span>(html)</span><br><span class="line">    dynamics = selector.xpath('//div[contains(<span class="meta">@class</span>, <span class="string">"news"</span>)]//div[contains(<span class="meta">@class</span>, <span class="string">"alert"</span>)]')</span><br><span class="line">    <span class="keyword">for</span> item <span class="keyword">in</span> dynamics:</span><br><span class="line">        dynamic = <span class="string">' '</span>.join(item.xpath('.//div[<span class="meta">@class</span>=<span class="string">"title"</span>]//text()')).strip()</span><br><span class="line">        print(dynamic)</span><br><span class="line"></span><br><span class="line">def profile(self, html):</span><br><span class="line">    selector = etree.<span class="built_in">HTML</span>(html)</span><br><span class="line">    name = selector.xpath('//input[<span class="meta">@id</span>=<span class="string">"user_profile_name"</span>]/<span class="meta">@value</span>')[<span class="number">0</span>]</span><br><span class="line">    email = selector.xpath('//select[<span class="meta">@id</span>=<span class="string">"user_profile_email"</span>]/option[<span class="meta">@value</span>!=<span class="string">""</span>]/text()')</span><br><span class="line">    print(name, email)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里，我们仍然使用 XPath 对信息进行提取。在 dynamics() 方法里，我们提取了所有的动态信息，然后将其遍历输出。在 prifile() 方法里，我们提取了个人的昵称和绑定的邮箱，然后将其输出。 这样，整个类的编写就完成了。</p>
                  <h3 id="5-运行"><a href="#5-运行" class="headerlink" title="5. 运行"></a>5. 运行</h3>
                  <p>我们新建一个 Login 对象，然后运行程序，如下所示：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">if</span> __name__ == "__main__":</span><br><span class="line">   <span class="keyword">login</span> = <span class="keyword">Login</span>()</span><br><span class="line">   <span class="keyword">login</span>.<span class="keyword">login</span>(email=<span class="string">'cqc@cuiqingcai.com'</span>, <span class="keyword">password</span>=<span class="string">'password'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在 login() 方法传入用户名和密码，实现模拟登录。 可以看到控制台有类似如下输出：</p>
                  <figure class="highlight properties">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">GrahamCampbell</span>  <span class="string">starred  nunomaduro/zero-framework</span></span><br><span class="line"><span class="attr">GrahamCampbell</span>  <span class="string">starred  nunomaduro/laravel-zero</span></span><br><span class="line"><span class="attr">happyAnger6</span>  <span class="string">created repository  happyAnger6/nodejs_chatroom</span></span><br><span class="line"><span class="attr">viosey</span>  <span class="string">starred  nitely/Spirit</span></span><br><span class="line"><span class="attr">lbgws2</span>  <span class="string">starred  Germey/TaobaoMM</span></span><br><span class="line"><span class="attr">EasyChris</span>  <span class="string">starred  ageitgey/face_recognition</span></span><br><span class="line"><span class="attr">callmewhy</span>  <span class="string">starred  macmade/GitHubUpdates</span></span><br><span class="line"><span class="attr">sindresorhus</span>  <span class="string">starred  sholladay/squatter</span></span><br><span class="line"><span class="attr">SamyPesse</span>  <span class="string">starred  graphcool/chromeless</span></span><br><span class="line"><span class="attr">wbotelhos</span>  <span class="string">starred  tkadlec/grunt-perfbudget</span></span><br><span class="line"><span class="attr">wbotelhos</span>  <span class="string">created repository  wbotelhos/eggy</span></span><br><span class="line"><span class="attr">leohxj</span>  <span class="string">starred  MacGesture/MacGesture</span></span><br><span class="line"><span class="attr">GrahamCampbell</span>  <span class="string">starred  GrahamCampbell/Analyzer</span></span><br><span class="line"><span class="attr">EasyChris</span>  <span class="string">starred  golang/go</span></span><br><span class="line"><span class="attr">mitulgolakiya</span>  <span class="string">starred  veltman/flubber</span></span><br><span class="line"><span class="attr">liaoyuming</span>  <span class="string">pushed to  student  at  Germey/SecurityCourse</span></span><br><span class="line"><span class="attr">leohxj</span>  <span class="string">starred  jasonslyvia/a-cartoon-intro-to-redux-cn</span></span><br><span class="line"><span class="attr">ruanyf</span>  <span class="string">starred  ericchiang/pup</span></span><br><span class="line"><span class="attr">ruanyf</span>  <span class="string">starred  bpesquet/thejsway</span></span><br><span class="line"><span class="attr">louwailou</span>  <span class="string">forked  Germey/ScrapyTutorial  to  louwailou/ScrapyTutorial</span></span><br><span class="line"><span class="attr">Lving</span>  <span class="string">forked  shadowsocksr-backup/shadowsocksr  to  Lving/shadowsocksr</span></span><br><span class="line"><span class="attr">qifuren1985</span>  <span class="string">starred  Germey/ADSLProxyPool</span></span><br><span class="line"><span class="attr">QWp6t</span>  <span class="string">starred  laravel/framework</span></span><br><span class="line"><span class="attr">Germey</span> <span class="string">['1016903103@qq.com', 'cqc@cuiqingcai.com']</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以发现，我们成功获取到关注的人的动态信息和个人的昵称及绑定邮箱。模拟登录成功！</p>
                  <h3 id="6-本节代码"><a href="#6-本节代码" class="headerlink" title="6. 本节代码"></a>6. 本节代码</h3>
                  <p>本节代码地址：<a href="https://github.com/Python3WebSpider/GithubLogin" target="_blank" rel="noopener">https://github.com/Python3WebSpider/GithubLogin</a>。</p>
                  <h3 id="7-结语"><a href="#7-结语" class="headerlink" title="7. 结语"></a>7. 结语</h3>
                  <p>我们利用 requests 的 Session 实现了模拟登录操作，其中最重要的还是分析思路，只要各个参数都成功获取，那么模拟登录是没有问题的。 登录成功，这就相当于建立了一个 Session 会话，Session 对象维护着 Cookies 的信息，直接请求就会得到模拟登录成功后的页面。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-24 23:45:25" itemprop="dateCreated datePublished" datetime="2019-11-24T23:45:25+08:00">2019-11-24</time>
                </span>
                <span id="/8229.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 10.1-模拟登录并爬取 GitHub" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>4.9k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>4 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8155.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/8155.html" class="post-title-link" itemprop="url">如何分离 Git 子目录</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>最近工作遇到了一个问题。对我们公司的开发小组来说，整个小组的人员都在一个 Repository 下面协作，这个 Repository 里面的文件夹非常多，而我只负责其中的一个功能的开发，我开发的功能所在的文件夹是可以独立维护的，它不依赖于 Repository 里面的其他的任何一个文件夹。</p>
                  <p>现在我新招到了一位实习生，会跟我一同做这个功能。但很尴尬的是，原则上来说实习生是不能有整个 Repository 的权限的，因为其他的文件夹下可能有包含一些关键信息，那我又怎么把我的这一部分的代码共享给他呢？</p>
                  <p>有的小伙伴可能说可以用软连接，但是用软连接的话实际上是不行的，因为 git 在 commit 软连接的时候会把它当成文件对待的。</p>
                  <p>比如说我有一个文件夹啊，我创建了一个软连接到这个文件夹，创建的链接文件实际上是不能以文件夹的形式提交到 Git 仓库的。</p>
                  <p>那么怎么办呢？硬链接就好了。</p>
                  <p>我使用的是 Mac OS 系统，可选的方案有 hln、bindfs，但前者是不能链接文件夹的。</p>
                  <p>一个比较可行的方案就是使用 bindfs，安装方法如下：</p>
                  <figure class="highlight mipsasm">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">brew </span><span class="keyword">install </span><span class="keyword">bindfs</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>然后使用如下命令即可：</p>
                  <figure class="highlight armasm">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">bindfs </span>source target</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样的话，比如我大库里面有个文件夹，名字叫做 foo，我就可以在我其他的目录下创建一个对该目录的挂载点 bar。</p>
                  <figure class="highlight awk">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">bindfs <span class="regexp">/var/</span>project1<span class="regexp">/foo /</span>var<span class="regexp">/project2/</span>bar</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我在 project1 下修改 foo 文件夹下的内容，project2 下的 bar 文件夹下的内容也会跟着修改了，我只需要把想要链接的文件夹都放在 project2 下，project2 作为一个独立的 Git 仓库，实习生只能看到我分离出来的内容，看不到大库 project1 下的内容。</p>
                  <p>这样如果实习生更新了 project2 的 bar 文件夹，提交到了 project2 对应的 Git 仓库，我从上面 pull 下代码，这样 project1 里面的 foo 文件夹也会跟着更新了，这样我再把新的改动提交到 project1 即可。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-18 20:32:59" itemprop="dateCreated datePublished" datetime="2019-11-18T20:32:59+08:00">2019-11-18</time>
                </span>
                <span id="/8155.html" class="post-meta-item leancloud_visitors" data-flag-title="如何分离 Git 子目录" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>830</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>1 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8063.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/8063.html" class="post-title-link" itemprop="url">利用深度学习识别验证码缺口</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>做爬虫的同学肯定或多或少会为验证码苦恼过，在最初的时候，大部分验证码都是图形验证码。但是前几年「极验」验证码横空出世，行为验证码变得越来越流行，其中之一的形式便是滑块验证码。 滑块验证码是怎样的呢？如图所示，验证码是一张矩形图，图片左侧会出现一个滑块，右侧会出现一个缺口，下侧会出现一个滑轨。左侧的滑块会随着滑轨的拖动而移动，如果能将左侧滑块正好滑动到右侧缺口处，就算完成了验证。 <img src="https://qiniu.cuiqingcai.com/2019-11-06-183053.png" alt="image-20191107023051548"> 由于这种验证码交互形式比较友好，且安全性、美观度上也会更高，像这种类似的验证码也变得越来越流行。另外不仅仅是「极验」，其他很多验证码服务商也推出了类似的验证码服务，如「网易易盾」等，上图所示的就是「网易易盾」的滑动验证码。 没错，确实这种滑动验证码的出现让很多网站变得更安全。但是做爬虫的可就苦恼了，如果采用自动化的方法来绕过这种滑动验证码，关键部分在于以下两点：</p>
                  <ul>
                    <li>找出目标缺口的位置。</li>
                    <li>模拟人的滑动轨迹将滑块滑动到缺口处。</li>
                  </ul>
                  <p>那么问题来了，第一步怎么做呢？ 我们怎么识别目标缺口到底在图片的哪个地方？大家可能想到的答案有：</p>
                  <ul>
                    <li>直接手工一把梭。</li>
                    <li>利用图像处理算法检测缺口处特征。</li>
                    <li>对接打码平台，获取缺口位置。</li>
                  </ul>
                  <p>另外对于极验来说，之前还有一种方法来识别缺口，那就是对比原图和缺口图的不同之处，通过遍历像素点来找出缺口的位置，但这种方法就比较投机了。如果换家验证码服务商，不给我们原图，我们就无从比较计算了。 总之，我们的目标就是输入一张图，输出缺口的的位置。 上面的方法呢，要么费时费钱、要么准确率不高。那还有没有其他的解决方案呢？ 当然有。 现在深度学习这么火，基于深度学习的图像识别技术已经发展得比较成熟了。那么我们能不能利用它来识别缺口位置呢？ 答案是，没问题，我们只需要将这个问题归结成一个深度学习的「目标检测」问题就好了。 听到这里，现在可能有的同学已经望而却步了，深度学习？我一点基础都没有呀，咋办？ 不用担心，本节介绍的内容全程没有一行代码，不需要任何深度学习基础，我们只需要动动手点一点就能搭建一个识别验证码缺口的深度学习的模型。 这么神奇？是的，那么本节我就带大家来实现一下吧。</p>
                  <h2 id="目标检测"><a href="#目标检测" class="headerlink" title="目标检测"></a>目标检测</h2>
                  <p>首先在开始之前简单说下目标检测。什么叫目标检测？顾名思义，就是把我们想找的东西找出来。比如给一张「狗」的图片，如图所示： <img src="https://qiniu.cuiqingcai.com/2019-11-06-184842.png" alt="image-20191107024841075"> 我们想知道这只狗在哪，它的舌头在哪，找到了就把它们框选出来，这就是目标检测。 经过目标检测算法处理之后，我们期望得到的图片是这样的： <img src="https://qiniu.cuiqingcai.com/2019-11-06-185010.png" alt="image-20191107025008947"> 可以看到这只狗和它的舌头就被框选出来了，这就完成了一个不错的目标检测。 现在比较流行的目标检测算法有 R-CNN、Fast R-CNN、Faster R-CNN、SSD、YOLO 等，感兴趣同学的可以了解一下，当然看不懂也没有什么影响。 另外再提一个地方，不懂深度学习的同学可以看看，懂的直接跳过下面一段。 我们既然要搭建一个模型来实现一个目标检测算法，那模型怎么知道我们究竟想识别个什么东西？就比如上图，模型咋知道我们想识别的是狗而不是草，是舌头而不是鼻子。这是因为，既然叫深度学习，那得有学习的东西。所以，搭建一个深度学习模型需要训练数据。啥也不告诉模型，模型从哪里去学习？所以，我们得预先有一些标注好位置的图片供模型去学习（训练），比如准备好多张狗的图片和狗的轮廓标注位置，模型在训练过程中会自动学习到图片和标注位置的关系。模型训练好了之后，我们给模型一个没有见过的类似的狗的图，模型也能找出来目标的位置了。 所以，迁移到验证码缺口识别这个任务上来，我们第一步就是给模型提供一些训练数据，训练数据就包括验证码的图片和缺口的位置标注轮廓信息。 好，既然如此，我们第一步就得准备一批验证码数据供标注和训练了。</p>
                  <h2 id="准备训练数据"><a href="#准备训练数据" class="headerlink" title="准备训练数据"></a>准备训练数据</h2>
                  <p>这里我用的是网易易盾的验证码，链接为：<a href="http://dun.163.com/trial/jigsaw" target="_blank" rel="noopener">http://dun.163.com/trial/jigsaw</a>。 我写爬虫爬下来了一些验证码的图，具体怎么爬的就不再赘述了，简单粗暴直接干就行了。 爬下来的验证码图类似这样子： <img src="https://qiniu.cuiqingcai.com/2019-11-06-190724.png" alt="image-20191107030722603"> 我们不需要滑轨的部分，只保留验证码本身的图片和上面的两个缺口就行了，下面是我准备的一些验证码图： <img src="https://qiniu.cuiqingcai.com/2019-11-06-190827.png" alt="image-20191107030825681"> 我爬了大约上千张吧，越多越好。当然对于今天的任务来说，其实几十上百张已经就够了。</p>
                  <h2 id="标注缺口位置"><a href="#标注缺口位置" class="headerlink" title="标注缺口位置"></a>标注缺口位置</h2>
                  <p>下一步就是把缺口的位置标注出来了。想一想这一步又不太好办，我难道还得每张图片量一量吗？这费了劲了，那咋整啊？ 很多同学可能到了这一步就望而却步了，更别提后面的搭建模型训练了。 但我们在文章开头说了，我们不需要写一行代码，点一点就能把模型搭建好。怎么做到的呢？我们可以借助于一些平台和工具。 在这里就要请出今天的主角—— ModelArts 了，这是我发现的华为云的一个深度学习平台，借助它我们可以完成数据标注、模型训练、模型部署三个步骤，最重要的是，我们不需要写代码，只需要点来点去就可以完成了。 让我们进入 ModelArts 来看看： <img src="https://qiniu.cuiqingcai.com/2019-11-06-191803.png" alt="image-20191107031802815"> 它已经内置了一些深度学习模型，包括图像分类、物体检测、预测分析等等，我们可以直接利用它们来快速搭建属于自己的模型。 在这里我们就切换到「自动学习」的选项卡，创建一个物体检测的项目。 <img src="https://qiniu.cuiqingcai.com/2019-11-06-192043.png" alt="image-20191107032040036"> 进入项目里面，可以看到最上面会显示三个步骤：</p>
                  <ul>
                    <li>数据标注</li>
                    <li>模型训练</li>
                    <li>部署上线</li>
                  </ul>
                  <p>也就是说，经过这三步，我们就可以搭建和部署一个深度学习模型。 页面如图所示： <img src="https://qiniu.cuiqingcai.com/2019-11-06-192250.png" alt="image-20191107032248156"> 那我们先来第一步——数据标注，这里我把一些验证码的图上传到页面中，在这里我上传了 112 张图： <img src="https://qiniu.cuiqingcai.com/2019-11-06-192409.png" alt="image-20191107032407896"> 上传完毕之后我们可以点击每一张图片进行标注了，这个平台提供了非常方便的标注功能，只需要鼠标拖拽个轮廓就完成了，112 张图标注完也就几分钟，标注的时候就框选这么个轮廓就行了，如图所示： <img src="https://qiniu.cuiqingcai.com/2019-11-06-192558.png" alt="image-20191107032556453"> 在这里边界需要把整个缺口的图全框选出来，其中上边界和右边界和标注框相切即可，总之确保标注框正好把缺口图框选出来就行，平台会自动保存和记录标注的像素点位置。 标注完一个，它会提示要添加一个名字，我在这里添加的名字叫「边界」，可以随意指定。 等全部标注完毕，点击「保存并返回」按钮即可。</p>
                  <h2 id="训练"><a href="#训练" class="headerlink" title="训练"></a>训练</h2>
                  <p>好，标注完了我们就可以开始训练了。我们在这里不需要写任何的代码，因为平台已经给我们写好了，内置了目标检测的深度学习模型，我们只需要提供数据训练就行了，如图所示： <img src="https://qiniu.cuiqingcai.com/2019-11-06-193006.png" alt="image-20191107033005181"> 在这里，我们只需要设置一下「最大训练时长」就好了，这么点图片其实几分钟就能训练完了，「最大训练时长」随意填写即可，最小不小于 0.05，填写完了之后就可以点击「开始训练」按钮训练了。 等几分钟，就会训练完成了，可以看到类似如图的页面： <img src="https://qiniu.cuiqingcai.com/2019-11-06-193212.png" alt="image-20191107033211474"> 这里显示了模型的各个参数和指标。 是的，你没看错，我们没有写任何代码，只过了几分钟，模型就已经训练完，并且可以部署上线了。</p>
                  <h2 id="部署测试"><a href="#部署测试" class="headerlink" title="部署测试"></a>部署测试</h2>
                  <p>然后进行下一步，部署上线，直接点击左上角的部署按钮即可： <img src="https://qiniu.cuiqingcai.com/2019-11-06-193412.png" alt="image-20191107033411530"> 过一会儿， 部署成功之后便可以看到类似这样的界面： <img src="https://qiniu.cuiqingcai.com/2019-11-06-193447.png" alt="image-20191107033446107"> 在这里我们可以上传任意的验证码图片进行测试，比如我随意上传一张没有标注过的验证码图，然后它会给我们展示出预测结果，如图所示： <img src="https://qiniu.cuiqingcai.com/2019-11-06-193909.png" alt="image-20191107033907756"> 可以看到，它就把缺口的位置检测出来了，同时在右侧显示了具体的像素值和置信度：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"detection_classes"</span>: [</span><br><span class="line">        <span class="string">"边界"</span></span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">"detection_boxes"</span>: [</span><br><span class="line">        [</span><br><span class="line">            <span class="number">16.579784393310547</span>,</span><br><span class="line">            <span class="number">331.89569091796875</span>,</span><br><span class="line">            <span class="number">124.46369934082031</span>,</span><br><span class="line">            <span class="number">435.0449523925781</span></span><br><span class="line">        ]</span><br><span class="line">    ],</span><br><span class="line">    <span class="attr">"detection_scores"</span>: [</span><br><span class="line">        <span class="number">0.9999219179153442</span></span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>是的，检测的结果还是比较准确的。有了这个结果，我们下一步模拟滑动到标注结果的左边界位置就好了！具体的模拟过程这里就不展开讲了。 另外平台同时还提供了模型部署后的 URL 接口和接口调用指南，也就是我们只需要向接口上传任意的验证码图片，就可以得到缺口的位置了！调用方式可以见：<a href="https://support.huaweicloud.com/engineers-modelarts/modelarts_23_0063.html" target="_blank" rel="noopener">https://support.huaweicloud.com/engineers-modelarts/modelarts_23_0063.html</a>。 嗯，就是这样，我们通过非常简单的操作，不需要任何代码，几分钟就搭建了一个深度学习模型，准确率也还不错。 当然这里我们只标注了 100 多张，标注得越多，标注得越精确，模型的准确率也会越高的。 以上便是利用 ModelArts 搭建滑动验证码缺口识别模型的方法，十分简洁高效。大家感兴趣可以了解下 ModelArts：<a href="https://www.huaweicloud.com/product/modelarts.html" target="_blank" rel="noopener">https://www.huaweicloud.com/product/modelarts.html</a>。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-08 18:38:23" itemprop="dateCreated datePublished" datetime="2019-11-08T18:38:23+08:00">2019-11-08</time>
                </span>
                <span id="/8063.html" class="post-meta-item leancloud_visitors" data-flag-title="利用深度学习识别验证码缺口" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>3.4k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>3 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8012.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/8012.html" class="post-title-link" itemprop="url">实战！手把手带你搭建图像分类 AI 服务</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>人工智能技术（以下称 AI）是人类优秀的发现和创造之一，它代表着至少几十年的未来。在传统的编程中，工程师将自己的想法和业务变成代码，计算机会根据代码设定的逻辑运行。与之不同的是，AI 使计算机有了「属于自己的思想」，它就像生物一样，能够「看」、「听」、「说」、「动」、「理解」、「分辨」和「思考」。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8krv9b06uj309q0a2gma.jpg" alt=""> AI 在图像识别和文本处理方面的效果尤为突出，且已经应用到人类的生活中，例如人脸识别、对话、车牌识别、城市智慧大脑项目中的目标检测和目标分类等。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8ks0uuxckj30fu09mt97.jpg" alt=""> <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8krzm9n5pj30zq0ee0x3.jpg" alt=""> 接下来，我们将了解图像分类的需求、完成任务的前提条件和任务实践。</p>
                  <h2 id="图像分类以及目标检测的需求"><a href="#图像分类以及目标检测的需求" class="headerlink" title="图像分类以及目标检测的需求"></a>图像分类以及目标检测的需求</h2>
                  <p>AI 的能力和应用都非常广泛，这里我们主要讨论的是图像分类。 图像分类，其实是对图像中主要目标的识别和归类。例如在很多张随机图片中分辨出哪一张中有直升飞机、哪一张中有狗。或者给定一张图片，让计算机分辨图像中主要目标的类别。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8krb83b8aj31800osth4.jpg" alt=""> 目标检测，指的是检测目标在图片中的位置。例如智慧交通项目中，路面监控摄像头拍摄画面中车辆的位置。目标检测涉及两种技术：分类和定位。也就是说先判定图片中是否存在指定的目标，然后还需要确定目标在图片中的位置。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8ks2pv2iwj31tq0s8gsj.jpg" alt=""> 这样的技术将会应用在人脸识别打卡、视频监控警报、停车场、高速收费站和城市智慧交通等项目当中。</p>
                  <h2 id="计算机识图的步骤"><a href="#计算机识图的步骤" class="headerlink" title="计算机识图的步骤"></a>计算机识图的步骤</h2>
                  <p>我们可以将计算机的看作是一个小朋友，它在拥有「分辨」的能力之前，必须经历「看」和「认识」这两个步骤，在看过很多图片后，它就会形成自己的「认知」，也就是获得了「分辨」能力。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8ks3o2wyfj30h209ajsx.jpg" alt=""> 简单来说，AI 工程师必须准备很多张不同的图片，并且将一大部分图片中的目标标注出来，然后让计算机提取每张图片中的特征，最后就会形成「认知」。 想一想，你还小的时候，是如何分辨鸭子和鹅的呢？ <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8ks4nckjyj30qq08aacn.jpg" alt=""> 是不是根据它们的特征进行判断的？</p>
                  <h2 id="学习和编程实现任务需要的条件"><a href="#学习和编程实现任务需要的条件" class="headerlink" title="学习和编程实现任务需要的条件"></a>学习和编程实现任务需要的条件</h2>
                  <p>了解完需求和步骤之后，我们还需要准备一些条件：</p>
                  <ul>
                    <li>首先，你必须是一名 IT 工程师。</li>
                    <li>然后你有一定的数学和统计学习基础。</li>
                    <li>你还得了解计算机处理图像的方式。</li>
                    <li>如果图片较多，你需要一台拥有较高算力 GPU 的计算机，否则计算机的「学习」速度会非常慢。</li>
                  </ul>
                  <p>具备以上条件后，再通过短时间（几天或一周）的学习，我们就能够完成图像分类的任务。 讨论个额外的话题，人人都能够做 AI 工程师吗？ AI 的门槛是比较高的，首先得具备高等数学、统计学习和编程等基础，然后要有很强的学习能力。对于 IT 工程师来说：</p>
                  <ul>
                    <li>编程基础是没有问题的</li>
                    <li>学习能力看个人，但花时间、下功夫肯定会有进步</li>
                    <li>高等数学基础，得好好补</li>
                    <li>统计学习基础，也得好好补</li>
                    <li>经济上无压力</li>
                  </ul>
                  <p>如果你想要成为一名 AI 工程师，那么「高学历」几乎是必备的。无论是一线互联网企业或者新崛起的 AI 独角兽，它们为 AI 工程师设立的学历门槛都是「硕士」。除非特别优秀的、才华横溢的大专或本科生，否则是不可能有机会进入这样的企业做 AI 工程师的。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8ks5vlpo0j30ek088gm9.jpg" alt=""> AI 在硬件、软件、数据资料和人才方面都是很费钱的，普通的 IT 工程师也就是学习了解一下，远远达不到产品商用的要求。 普通的中小企业，极少有资质和经济能力吸引高学历且优秀的 AI 工程师，这就导致了资源的聚拢和倾斜。 想要将图像分类技术商用，在让计算机经历「看」、「认识」的步骤并拥有「分辨」能力后，还要将其转换为 Web 服务。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8ks6xmjhfj316y0godlw.jpg" alt=""> 但我只想将人脸识别或者图像分类的功能集成到我的项目当中，就那么困难吗？ 我只是一个很小的企业，想要在原来普通的视频监控系统中增加「家人识别」、「陌生人警报」、「火灾警报」和「生物闯入提醒」等功能，没有上述的条件和经济投入，就不能实现了吗？ 我好苦恼！ 有什么好办法吗？</p>
                  <h2 id="ModelArts-简介和条件"><a href="#ModelArts-简介和条件" class="headerlink" title="ModelArts 简介和条件"></a>ModelArts 简介和条件</h2>
                  <p>ModelArts 是华为云推出的产品，它是面向开发者的一站式 AI 开发平台。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8ks7ujxpwj31to0b440n.jpg" alt=""> 它为机器学习与深度学习提供海量数据预处理及半自动化标注、大规模分布式 Training、自动化模型生成，及端-边-云模型按需部署能力，帮助用户快速创建和部署模型，管理全周期 AI 工作流。 它为用户提供了以下可选模式：</p>
                  <ul>
                    <li>零编码经验、零 AI 经验的自动学习模式</li>
                    <li>有 AI 研发经验的全流程开发模式</li>
                  </ul>
                  <p><img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8ksaaafekj31ve0u0gu1.jpg" alt=""> <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8ks9j6iguj31qe0dsdi2.jpg" alt=""> 同时，它将 AI 开发的整个过程都集成了进来。例如数据标注、模型训练、参数优化、服务部署、开放接口等，这就是「全周期 AI 工作流」。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8ks8i8yrpj31kv0u0wn2.jpg" alt=""> 还有，平台上的操作都是可视化的。 这些条件对于想要将 AI 技术应用于产品，但无奈条件不佳的个人开发者和企业提供了机会，这很重要！可以说 <a href="[https://www.huaweicloud.com/product/modelarts.html](https://www.huaweicloud.com/product/modelarts.html">ModelArts</a>) 缩短了 AI 商用的时间，降低了对应的经济成本、时间成本和人力成本。 更贴心的是，华为云 <a href="[https://www.huaweicloud.com/product/modelarts.html](https://www.huaweicloud.com/product/modelarts.html">ModelArts</a>) 为用户准备了很多的教程。即使用户没有经验，但只要按照教程指引进行操作，也能够实现自己的 AI 需求。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8ks907u59j31m90u0468.jpg" alt=""> 想想就美滋滋，太棒了！ 赶紧体验一下！</p>
                  <h2 id="图像分类服务实践"><a href="#图像分类服务实践" class="headerlink" title="图像分类服务实践"></a>图像分类服务实践</h2>
                  <p>这次我们以零 AI 基础和零编码经验的自动学习模式演示如何搭建一个图像分类的 AI 服务。</p>
                  <h3 id="前期准备和相关设置"><a href="#前期准备和相关设置" class="headerlink" title="前期准备和相关设置"></a>前期准备和相关设置</h3>
                  <p>首先打开华为云官网，将鼠标移动导航栏的「EI 企业智能」菜单上，并在弹出的选项中选择「AI 开发平台 ModelArts」。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kr80qn02j31so0u045b.jpg" alt=""> 进入到 <a href="[https://www.huaweicloud.com/product/modelarts.html](https://www.huaweicloud.com/product/modelarts.html">ModelArts</a>) 主页后，可以浏览一下关于 <a href="[https://www.huaweicloud.com/product/modelarts.html](https://www.huaweicloud.com/product/modelarts.html">ModelArts</a>) 的介绍。 点击 Banner 处的「进入控制台」按钮，页面会跳转到 ModelArts 控制台。控制台大体分为几个区域： <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kr8u1cu5j31g90u0aqf.jpg" alt=""> 区域 2 自动学习模式中有图像分类，将鼠标移动到图标上，并点击弹出的「开始体验」按钮。如果是华为云的新用户，网页会提示我们输入访问密钥和私有访问密钥。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kr948a21j312s0jktaw.jpg" alt=""> 没有密钥的开发者可以点击页面给出的链接并按照指引获取密钥，得到两种密钥后将其填入框中，点击「确定」按钮即可。 此时正式进入项目创建流程中，点击「图像分类」中的「创建项目」按钮（华为云为用户准备了对应的教程，很贴心）。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kr9gpk16j31iu0iyn0j.jpg" alt=""> 在创建项目的页面中，我们需要填两三项配置。要注意的是，项目是按需计费的，这次我们只是体验，也没有训练和存储太多数据，所以费用很低，大家不用担心。 项目名称可以根据需求设定一个容易记的，案例中我将其设定为 ImageCLF-Test-Pro。在训练数据的存储选择处，点击输入框中的文件夹图标，在弹出的选项卡中新建 obs 桶 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kr9o03hfj321o0m4aeb.jpg" alt=""> 并在创建的桶中新建文件夹 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kra1rz5jj32200ogn26.jpg" alt=""> 最后输入描述，并点击页面右下角的「创建项目」按钮即可。</p>
                  <h3 id="上传图片和标注"><a href="#上传图片和标注" class="headerlink" title="上传图片和标注"></a>上传图片和标注</h3>
                  <p>项目创建好之后，我们需要准备用于训练的多张图片，图片尽量清晰、种类超过 2 类、每种分类的图片数量不少于 5 张。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kranz44wj31wm0u0n1w.jpg" alt=""> 当然，数据越多、形态越丰富、标注越准确，那么训练结果就会越好，AI 服务的体验就会越好。 这里我准备了一些直升机、坦克和狗的图片，共 45 张。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8krb83b8aj31800osth4.jpg" alt=""> 将其批量导入后勾选同类型的图片，一次性为多张图添加标签。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8krbj4ng2j31p90u0qgn.jpg" alt=""> 依次将 3 类图片标注后，左侧图片标注的「未标注」选项卡中的图就会清空，而「已标注」选项卡中可以看到标注好的图片。</p>
                  <h3 id="训练设置"><a href="#训练设置" class="headerlink" title="训练设置"></a>训练设置</h3>
                  <p>右侧的标签栏会显示每种分类和对应的图片数量，下方的训练设置可以让我们设置训练时长的上限，高级设置中还有推理时间。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8krbt7olrj30jc0nct9q.jpg" alt=""> 这个我们不必理解它的作用，可以按照默认值进行，也可以稍微调整，例如将训练时长的上限改为 0.2。</p>
                  <h3 id="开始训练"><a href="#开始训练" class="headerlink" title="开始训练"></a>开始训练</h3>
                  <p>设置好后点击「开始训练」按钮就会进入训练状态，耐心等待一段时间（图片越少训练时间越短）。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8krc7gotwj30m207sdgg.jpg" alt=""> 训练页左侧会显示训练状态，例如初始化、运行中和运行成功/失败等。训练完成后，右侧会给出运行时长、准确率、评估结果和训练参数等信息。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8krcjd7aoj31bs0u0aen.jpg" alt=""></p>
                  <h3 id="服务的自动化部署"><a href="#服务的自动化部署" class="headerlink" title="服务的自动化部署"></a>服务的自动化部署</h3>
                  <p>我们的目的是搭建一个图像分类的 AI 服务，所以在训练结束后点击左侧的「部署」按钮，此时会进入自动化部署的流程。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8krd9a59rj31820u078z.jpg" alt=""> 稍微等待些许时间（本次约 10 分钟）后，页面提示部署完成，同时页面将会分为 3 栏。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kre5gknwj31le0u00z8.jpg" alt=""> 左侧 1 区为部署状态和控制。中间 2 区可以在线测试图片分类，右侧 3 区会显示在线测试的结果（包括准确率），右侧 4 区提供了 API 接口，方便我们将其集成到 Web 应用当中。</p>
                  <h3 id="在线预测，训练结果测试"><a href="#在线预测，训练结果测试" class="headerlink" title="在线预测，训练结果测试"></a>在线预测，训练结果测试</h3>
                  <p>我们来测试一下，准备几张没有经过标注的图片，图片中可以包含狗、直升机和坦克。点击中间 2 区的「上传」按钮并选择一张图片，然后点击「预测」按钮。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kredfp9pj314d0u0101.jpg" alt=""> 1 秒中不到，右侧 3 区就会返回本次预测的结果：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"predicted_label"</span>: <span class="string">"狗"</span>,</span><br><span class="line">    <span class="attr">"scores"</span>: [</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"狗"</span>,</span><br><span class="line">            <span class="string">"0.840"</span></span><br><span class="line">        ],</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"直升机"</span>,</span><br><span class="line">            <span class="string">"0.084"</span></span><br><span class="line">        ],</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"坦克"</span>,</span><br><span class="line">            <span class="string">"0.076"</span></span><br><span class="line">        ]</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这次我们上传的是包含狗的图片，返回的预测结果中显示本次预测的标签是「狗」，并且列出了可信度较高的几个类别和对应的可信度（1 为 100% 肯定），其中最高的是 「0.840-狗」。 这次上传直升机的图片试试。 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kreijel6j314m0u0qb3.jpg" alt=""> 返回的预测结果如下：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"predicted_label"</span>: <span class="string">"直升机"</span>,</span><br><span class="line">    <span class="attr">"scores"</span>: [</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"直升机"</span>,</span><br><span class="line">            <span class="string">"0.810"</span></span><br><span class="line">        ],</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"狗"</span>,</span><br><span class="line">            <span class="string">"0.114"</span></span><br><span class="line">        ],</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"坦克"</span>,</span><br><span class="line">            <span class="string">"0.075"</span></span><br><span class="line">        ]</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>再试试坦克 <img src="https://tva1.sinaimg.cn/large/006y8mN6ly1g8kreq9m0oj31510u0qbd.jpg" alt=""> 返回的预测结果如下：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"predicted_label"</span>: <span class="string">"坦克"</span>,</span><br><span class="line">    <span class="attr">"scores"</span>: [</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"坦克"</span>,</span><br><span class="line">            <span class="string">"0.818"</span></span><br><span class="line">        ],</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"狗"</span>,</span><br><span class="line">            <span class="string">"0.092"</span></span><br><span class="line">        ],</span><br><span class="line">        [</span><br><span class="line">            <span class="string">"直升机"</span>,</span><br><span class="line">            <span class="string">"0.090"</span></span><br><span class="line">        ]</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>从几次测试的结果可以看出，预测的结果非常准确，而且给出的可信度也比较高。这次准备的图片并不是很多，形态也不是很丰富，但预测效果却非常好，不得不说华为云 ModelArts 开发团队为此做了很多的优化，甚至比我自己（深度学习入门水平）编写代码用卷积神经网络训练和预测的结果要好。 如果想要将其集成到 Web 应用中，只需要根据页面给出的「接口调用指南」的指引进行操作即可。</p>
                  <h3 id="释放资源"><a href="#释放资源" class="headerlink" title="释放资源"></a>释放资源</h3>
                  <p>如果不是真正商用，仅仅作为学习和练习，那么在操作完成后记得点击左侧 1 区的「停止」按钮。然后在华为云导航栏中的搜索框输入「OBS」，点击搜索结果后跳转到 OBS 主页，接着再 OBS 主页点击「管理控制台」，进入到 OBS 控制台中，删除之前创建的桶即可。这样就不会导致资源占用，也不会产生费用了。</p>
                  <h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3>
                  <p>体验了一下 ModelArts，我感觉非常奈斯！ 每处都有提示或教程指引，操作过程流畅，没有出现卡顿、报错等问题。 批量数据标注太好用了！批量导入、批量标注，自动计数，舒服！ 训练速度很快，应该是用了云 GPU，这样就算我的电脑没有显卡也能够快速完成训练。 以前还在考虑，学习 AI 是否需要准备更强的硬件设备，现在好了，在 ModelArts 上操作，就不用考虑这些条件了。 本次我们体验的是自动学习，也就是简洁易用的傻瓜式操作。对于专业的 AI 工程师来说，可以选择全流程开发模式。批量数据标注、本地代码编写、本地调试、云端训练、云端部署等一气呵成。 棒！ 有兴趣的开发者可以前往华为云 <a href="[https://www.huaweicloud.com/product/modelarts.html](https://www.huaweicloud.com/product/modelarts.html">ModelArts</a>) 体验。</p>
                  <hr>
                  <p>备注：文中配图均出自互联网，通过搜索引擎而来。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/韦世东学算法和反爬虫" class="author" itemprop="url" rel="index">韦世东学算法和反爬虫</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-11-05 19:56:04" itemprop="dateCreated datePublished" datetime="2019-11-05T19:56:04+08:00">2019-11-05</time>
                </span>
                <span id="/8012.html" class="post-meta-item leancloud_visitors" data-flag-title="实战！手把手带你搭建图像分类 AI 服务" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>4.3k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>4 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/7844.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/7844.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 9.5-使用代理爬取微信公众号文章</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>前面讲解了代理池的维护和付费代理的相关使用方法，接下来我们进行一下实战演练，利用代理来爬取微信公众号的文章。</p>
                  <h3 id="1-本节目标"><a href="#1-本节目标" class="headerlink" title="1. 本节目标"></a>1. 本节目标</h3>
                  <p>我们的主要目标是利用代理爬取微信公众号的文章，提取正文、发表日期、公众号等内容，爬取来源是搜狗微信，其链接为 <a href="http://weixin.sogou.com/，然后把爬取结果保存到" target="_blank" rel="noopener">http://weixin.sogou.com/，然后把爬取结果保存到</a> MySQL 数据库。</p>
                  <h3 id="2-准备工作"><a href="#2-准备工作" class="headerlink" title="2. 准备工作"></a>2. 准备工作</h3>
                  <p>首先需要准备并正常运行前文中所介绍的代理池。这里需要用的 Python 库有 aiohttp、requests、redis-py、pyquery、Flask、PyMySQL，如这些库没有安装可以参考第 1 章的安装说明。</p>
                  <h3 id="3-爬取分析"><a href="#3-爬取分析" class="headerlink" title="3. 爬取分析"></a>3. 爬取分析</h3>
                  <p>搜狗对微信公众平台的公众号和文章做了整合。我们可以通过上面的链接搜索到相关的公众号和文章，例如搜索 NBA，可以搜索到最新的文章，如图 9-21 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-055950.png" alt=""> 图 9-21 搜索结果 点击搜索后，搜索结果的 URL 中其实有很多无关 GET 请求参数，将无关的参数去掉，只保留 type 和 query 参数，例如 <a href="http://weixin.sogou.com/weixin?type=2&amp;query=NBA，搜索关键词为" target="_blank" rel="noopener">http://weixin.sogou.com/weixin?type=2&amp;query=NBA，搜索关键词为</a> NBA，类型为 2，2 代表搜索微信文章。 下拉网页，点击下一页即可翻页，如图 9-22 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-060001.jpg" alt=""> 图 9-22 翻页列表 注意，如果没有输入账号登录，那只能看到 10 页的内容，登录之后可以看到 100 页内容，如图 9-23 和图 9-24 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-060006.jpg" alt=""> 图 9-23 不登录的结果 <img src="https://qiniu.cuiqingcai.com/2019-10-20-060011.jpg" alt=""> 图 9-24 登录后的结果 如果需要爬取更多内容，就需要登录并使用 Cookies 来爬取。 搜狗微信站点的反爬虫能力很强，如连续刷新，站点就会弹出类似如图 9-25 所示的验证。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-060018.png" alt=""> 图 9-25 验证码页面 网络请求出现了 302 跳转，返回状态码为 302，跳转的链接开头为 <a href="http://weixin.sogou.com/antispider/，这很明显就是一个反爬虫的验证页面。所以我们得出结论，如果服务器返回状态码为" target="_blank" rel="noopener">http://weixin.sogou.com/antispider/，这很明显就是一个反爬虫的验证页面。所以我们得出结论，如果服务器返回状态码为</a> 302 而非 200，则 IP 访问次数太高，IP 被封禁，此请求就是失败了。 如果遇到这种情况，我们可以选择识别验证码并解封，也可以使用代理直接切换 IP。在这里我们采用第二种方法，使用代理直接跳过这个验证。代理使用上一节所讲的代理池，还需要更改检测的 URL 为搜狗微信的站点。 对于这种反爬能力很强的网站来说，如果我们遇到此种返回状态就需要重试。所以我们采用另一种爬取方式，借助数据库构造一个爬取队列，待爬取的请求都放到队列里，如果请求失败了重新放回队列，就会被重新调度爬取。 在这里我们可以采用 Redis 的队列数据结构，新的请求就加入队列，或者有需要重试的请求也放回队列。调度的时候如果队列不为空，那就把一个个请求取出来执行，得到响应后再进行解析，提取出我们想要的结果。 这次我们采用 MySQL 存储，借助 PyMySQL 库，将爬取结果构造为一个字典，实现动态存储。 综上所述，我们本节实现的功能有如下几点。</p>
                  <ul>
                    <li>修改代理池检测链接为搜狗微信站点</li>
                    <li>构造 Redis 爬取队列，用队列实现请求的存取</li>
                    <li>实现异常处理，失败的请求重新加入队列</li>
                    <li>实现翻页和提取文章列表并把对应请求加入队列</li>
                    <li>实现微信文章的信息的提取</li>
                    <li>将提取到的信息保存到 MySQL</li>
                  </ul>
                  <p>好，那么接下来我们就用代码来实现一下。</p>
                  <h3 id="4-构造-Request"><a href="#4-构造-Request" class="headerlink" title="4. 构造 Request"></a>4. 构造 Request</h3>
                  <p>既然我们要用队列来存储请求，那么肯定要实现一个请求 Request 的数据结构，这个请求需要包含一些必要信息，如请求链接、请求头、请求方式、超时时间。另外对于某个请求，我们需要实现对应的方法来处理它的响应，所以需要再加一个 Callback 回调函数。每次翻页请求需要代理来实现，所以还需要一个参数 NeedProxy。如果一个请求失败次数太多，那就不再重新请求了，所以还需要加失败次数的记录。 这些字段都需要作为 Request 的一部分，组成一个完整的 Request 对象放入队列去调度，这样从队列获取出来的时候直接执行这个 Request 对象就好了。 我们可以采用继承 reqeusts 库中的 Request 对象的方式来实现这个数据结构。requests 库中已经有了 Request 对象，它将请求 Request 作为一个整体对象去执行，得到响应后再返回。其实 requests 库的 get()、post() 等方法都是通过执行 Request 对象实现的。 我们首先看看 Request 对象的源码：</p>
                  <figure class="highlight oxygene">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">class</span> Request(RequestHooksMixin):</span><br><span class="line">    def __init__(<span class="keyword">self</span>,</span><br><span class="line">            <span class="function"><span class="keyword">method</span>=<span class="title">None</span>, <span class="title">url</span>=<span class="title">None</span>, <span class="title">headers</span>=<span class="title">None</span>, <span class="title">files</span>=<span class="title">None</span>, <span class="title">data</span>=<span class="title">None</span>,</span></span><br><span class="line"><span class="function">            <span class="title">params</span>=<span class="title">None</span>, <span class="title">auth</span>=<span class="title">None</span>, <span class="title">cookies</span>=<span class="title">None</span>, <span class="title">hooks</span>=<span class="title">None</span>, <span class="title">json</span>=<span class="title">None</span>):</span></span><br><span class="line"></span><br><span class="line">        # <span class="keyword">Default</span> <span class="keyword">empty</span> dicts <span class="keyword">for</span> dict <span class="keyword">params</span>.</span><br><span class="line">        data = [] <span class="keyword">if</span> data <span class="keyword">is</span> None <span class="keyword">else</span> data</span><br><span class="line">        files = [] <span class="keyword">if</span> files <span class="keyword">is</span> None <span class="keyword">else</span> files</span><br><span class="line">        headers = <span class="comment">&#123;&#125;</span> <span class="keyword">if</span> headers <span class="keyword">is</span> None <span class="keyword">else</span> headers</span><br><span class="line">        <span class="keyword">params</span> = <span class="comment">&#123;&#125;</span> <span class="keyword">if</span> <span class="keyword">params</span> <span class="keyword">is</span> None <span class="keyword">else</span> <span class="keyword">params</span></span><br><span class="line">        hooks = <span class="comment">&#123;&#125;</span> <span class="keyword">if</span> hooks <span class="keyword">is</span> None <span class="keyword">else</span> hooks</span><br><span class="line"></span><br><span class="line">        <span class="keyword">self</span>.hooks = default_hooks()</span><br><span class="line">        <span class="keyword">for</span> (k, v) <span class="keyword">in</span> list(hooks.items()):</span><br><span class="line">            <span class="keyword">self</span>.register_hook(<span class="keyword">event</span>=k, hook=v)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">self</span><span class="function">.<span class="keyword">method</span> = <span class="title">method</span></span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">url</span> = <span class="title">url</span></span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">headers</span> = <span class="title">headers</span></span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">files</span> = <span class="title">files</span></span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">data</span> = <span class="title">data</span></span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">json</span> = <span class="title">json</span></span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">params</span> = <span class="title">params</span></span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">auth</span> = <span class="title">auth</span></span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">cookies</span> = <span class="title">cookies</span></span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这是 requests 库中 Request 对象的构造方法。这个 Request 已经包含了请求方式、请求链接、请求头这几个属性，但是相比我们需要的还差了几个。我们需要实现一个特定的数据结构，在原先基础上加入上文所提到的额外几个属性。这里我们需要继承 Request 对象重新实现一个请求，将它定义为 WeixinRequest，实现如下：</p>
                  <figure class="highlight oxygene">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">TIMEOUT = <span class="number">10</span></span><br><span class="line"><span class="keyword">from</span> requests import Request</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> WeixinRequest(Request):</span><br><span class="line">    def __init__(<span class="keyword">self</span>, url, callback, <span class="function"><span class="keyword">method</span>='<span class="title">GET</span>', <span class="title">headers</span>=<span class="title">None</span>, <span class="title">need_proxy</span>=<span class="title">False</span>, <span class="title">fail_time</span>=0, <span class="title">timeout</span>=<span class="title">TIMEOUT</span>):</span></span><br><span class="line">        Request.__init__(<span class="keyword">self</span>, <span class="function"><span class="keyword">method</span>, <span class="title">url</span>, <span class="title">headers</span>)</span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">callback</span> = <span class="title">callback</span></span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">need_proxy</span> = <span class="title">need_proxy</span></span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">fail_time</span> = <span class="title">fail_time</span></span></span><br><span class="line"><span class="function">        <span class="title">self</span>.<span class="title">timeout</span> = <span class="title">timeout</span></span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里我们实现了 WeixinRequest 数据结构。<strong>init</strong>() 方法先调用了 Request 的<strong>init</strong>() 方法，然后加入额外的几个参数，定义为 callback、need_proxy、fail_time、timeout，分别代表回调函数、是否需要代理爬取、失败次数、超时时间。 我们就可以将 WeixinRequest 作为一个整体来执行，一个个 WeixinRequest 对象都是独立的，每个请求都有自己的属性。例如，我们可以调用它的 callback，就可以知道这个请求的响应应该用什么方法来处理，调用 fail_time 就可以知道这个请求失败了多少次，判断失败次数是不是到了阈值，该不该丢弃这个请求。这里我们采用了面向对象的一些思想。</p>
                  <h3 id="5-实现请求队列"><a href="#5-实现请求队列" class="headerlink" title="5. 实现请求队列"></a>5. 实现请求队列</h3>
                  <p>接下来我们就需要构造请求队列，实现请求的存取。存取无非就是两个操作，一个是放，一个是取，所以这里利用 Redis 的 rpush() 和 lpop() 方法即可。 另外还需要注意，存取不能直接存 Request 对象，Redis 里面存的是字符串。所以在存 Request 对象之前我们先把它序列化，取出来的时候再将其反序列化，这个过程可以利用 pickle 模块实现。</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> pickle <span class="keyword">import</span> dumps, loads</span><br><span class="line"><span class="keyword">from</span> request <span class="keyword">import</span> WeixinRequest</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">RedisQueue</span><span class="params">()</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""初始化 Redis"""</span></span><br><span class="line">        self.db = StrictRedis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">add</span><span class="params">(self, request)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        向队列添加序列化后的 Request</span></span><br><span class="line"><span class="string">        :param request: 请求对象</span></span><br><span class="line"><span class="string">        :param fail_time: 失败次数</span></span><br><span class="line"><span class="string">        :return: 添加结果</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">if</span> isinstance(request, WeixinRequest):</span><br><span class="line">            <span class="keyword">return</span> self.db.rpush(REDIS_KEY, dumps(request))</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">pop</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        取出下一个 Request 并反序列化</span></span><br><span class="line"><span class="string">        :return: Request or None</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">if</span> self.db.llen(REDIS_KEY):</span><br><span class="line">            <span class="keyword">return</span> loads(self.db.lpop(REDIS_KEY))</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">empty</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="keyword">return</span> self.db.llen(REDIS_KEY) == <span class="number">0</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里实现了一个 RedisQueue，它的 <strong>init</strong>() 构造方法里面初始化了一个 StrictRedis 对象。随后实现了 add() 方法，首先判断 Request 的类型，如果是 WeixinRequest，那么就把程序就会用 pickle 的 dumps() 方法序列化，然后再调用 rpush() 方法加入队列。pop() 方法则相反，调用 lpop() 方法将请求从队列取出，然后再用 pickle 的 loads() 方法将其转为 WeixinRequest 对象。另外，empty() 方法返回队列是否为空，只需要判断队列长度是否为 0 即可。 在调度的时候，我们只需要新建一个 RedisQueue 对象，然后调用 add() 方法，传入 WeixinRequest 对象，即可将 WeixinRequest 加入队列，调用 pop() 方法，即可取出下一个 WeixinRequest 对象，非常简单易用。</p>
                  <h3 id="6-修改代理池"><a href="#6-修改代理池" class="headerlink" title="6. 修改代理池"></a>6. 修改代理池</h3>
                  <p>接下来我们要生成请求并开始爬取。在此之前还需要做一件事，那就是先找一些可用代理。 之前代理池检测的 URL 并不是搜狗微信站点，所以我们需要将代理池检测的 URL 修改成搜狗微信站点，以便于把被搜狗微信站点封禁的代理剔除掉，留下可用代理。 现在将代理池的设置文件中的 TEST_URL 修改一下，如 <a href="http://weixin.sogou.com/weixin?type=2&amp;amp" target="_blank" rel="noopener">http://weixin.sogou.com/weixin?type=2&amp;amp</a>; query=nba，被本站点封的代理就会减分，正常请求的代理就会赋值为 100，最后留下的就是可用代理。 修改之后将获取模块、检测模块、接口模块的开关都设置为 True，让代理池运行一会，如图 9-26 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-060026.jpg" alt=""> 图 9-26 代理池运行结果 这样，数据库中留下的 100 分的代理就是针对搜狗微信的可用代理了，如图 9-27 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-060049.jpg" alt=""> 图 9-27 可用代理列表 同时访问代理接口，接口设置为 5555，访问 <a href="http://127.0.0.1:5555/random，即可获取到随机可用代理，如图" target="_blank" rel="noopener">http://127.0.0.1:5555/random，即可获取到随机可用代理，如图</a> 9-28 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-060055.jpg" alt=""> 图 9-28 代理接口 再定义一个函数来获取随机代理：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">PROXY_POOL_URL = <span class="string">'http://127.0.0.1:5555/random'</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get_proxy</span><span class="params">(self)</span>:</span></span><br><span class="line">    <span class="string">"""</span></span><br><span class="line"><span class="string">    从代理池获取代理</span></span><br><span class="line"><span class="string">    :return:</span></span><br><span class="line"><span class="string">    """</span></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        response = requests.get(PROXY_POOL_URL)</span><br><span class="line">        <span class="keyword">if</span> response.status_code == <span class="number">200</span>:</span><br><span class="line">            print(<span class="string">'Get Proxy'</span>, response.text)</span><br><span class="line">            <span class="keyword">return</span> response.text</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line">    <span class="keyword">except</span> requests.ConnectionError:</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">None</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h3 id="7-第一个请求"><a href="#7-第一个请求" class="headerlink" title="7. 第一个请求"></a>7. 第一个请求</h3>
                  <p>一切准备工作都做好，下面我们就可以构造第一个请求放到队列里以供调度了。定义一个 Spider 类，实现 start() 方法的代码如下：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> requests <span class="keyword">import</span> Session</span><br><span class="line"><span class="keyword">from</span> db <span class="keyword">import</span> RedisQueue</span><br><span class="line"><span class="keyword">from</span> request <span class="keyword">import</span> WeixinRequest</span><br><span class="line"><span class="keyword">from</span> urllib.parse <span class="keyword">import</span> urlencode</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Spider</span><span class="params">()</span>:</span></span><br><span class="line">    base_url = <span class="string">'http://weixin.sogou.com/weixin'</span></span><br><span class="line">    keyword = <span class="string">'NBA'</span></span><br><span class="line">    headers = &#123;</span><br><span class="line">        <span class="string">'Accept'</span>: <span class="string">'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8'</span>,</span><br><span class="line">        <span class="string">'Accept-Encoding'</span>: <span class="string">'gzip, deflate'</span>,</span><br><span class="line">        <span class="string">'Accept-Language'</span>: <span class="string">'zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4,zh-TW;q=0.2,mt;q=0.2'</span>,</span><br><span class="line">        <span class="string">'Cache-Control'</span>: <span class="string">'max-age=0'</span>,</span><br><span class="line">        <span class="string">'Connection'</span>: <span class="string">'keep-alive'</span>,</span><br><span class="line">        <span class="string">'Cookie'</span>: <span class="string">'IPLOC=CN1100; SUID=6FEDCF3C541C940A000000005968CF55; SUV=1500041046435211; ABTEST=0|1500041048|v1; SNUID=CEA85AE02A2F7E6EAFF9C1FE2ABEBE6F; weixinIndexVisited=1; JSESSIONID=aaar_m7LEIW-jg_gikPZv; ld=Wkllllllll2BzGMVlllllVOo8cUlllll5G@HbZllll9lllllRklll5@@@@@@@@@@'</span>,</span><br><span class="line">        <span class="string">'Host'</span>: <span class="string">'weixin.sogou.com'</span>,</span><br><span class="line">        <span class="string">'Upgrade-Insecure-Requests'</span>: <span class="string">'1'</span>,</span><br><span class="line">        <span class="string">'User-Agent'</span>: <span class="string">'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36'</span></span><br><span class="line">    &#125;</span><br><span class="line">    session = Session()</span><br><span class="line">    queue = RedisQueue()</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">start</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""初始化工作"""</span></span><br><span class="line">        <span class="comment"># 全局更新 Headers</span></span><br><span class="line">        self.session.headers.update(self.headers)</span><br><span class="line">        start_url = self.base_url + <span class="string">'?'</span> + urlencode(&#123;<span class="string">'query'</span>: self.keyword, <span class="string">'type'</span>: <span class="number">2</span>&#125;)</span><br><span class="line">        weixin_request = WeixinRequest(url=start_url, callback=self.parse_index, need_proxy=<span class="literal">True</span>)</span><br><span class="line">        <span class="comment"># 调度第一个请求</span></span><br><span class="line">        self.queue.add(weixin_request)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里定义了 Spider 类，设置了很多全局变量，比如 keyword 设置为 NBA，headers 就是请求头。在浏览器里登录账号，然后在开发者工具里将请求头复制出来，记得带上 Cookie 字段，这样才能爬取 100 页的内容。然后初始化了 Session 和 RedisQueue 对象，它们分别用来执行请求和存储请求。 首先，start() 方法全局更新了 headers，使得所有请求都能应用 Cookies。然后构造了一个起始 URL：<a href="http://weixin.sogou.com/weixin?type=2&amp;query=NBA，随后用改" target="_blank" rel="noopener">http://weixin.sogou.com/weixin?type=2&amp;query=NBA，随后用改</a> URL 构造了一个 WeixinRequest 对象。回调函数是 Spider 类的 parse_index() 方法，也就是当这个请求成功之后就用 parse_index() 来处理和解析。need_proxy 参数设置为 True，代表执行这个请求需要用到代理。随后我们调用了 RedisQueue 的 add() 方法，将这个请求加入队列，等待调度。</p>
                  <h3 id="8-调度请求"><a href="#8-调度请求" class="headerlink" title="8. 调度请求"></a>8. 调度请求</h3>
                  <p>加入第一个请求之后，调度开始了。我们首先从队列中取出这个请求，将它的结果解析出来，生成新的请求加入队列，然后拿出新的请求，将结果解析，再生成新的请求加入队列，这样循环往复执行，直到队列中没有请求，则代表爬取结束。我们用代码实现如下：</p>
                  <figure class="highlight php">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">VALID_STATUSES = [<span class="number">200</span>]</span><br><span class="line"></span><br><span class="line">def schedule(<span class="keyword">self</span>):</span><br><span class="line">    <span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">    调度请求</span></span><br><span class="line"><span class="string">    :return:</span></span><br><span class="line"><span class="string">    "</span><span class="string">""</span></span><br><span class="line">    <span class="keyword">while</span> not <span class="keyword">self</span>.queue.<span class="keyword">empty</span>():</span><br><span class="line">        weixin_request = <span class="keyword">self</span>.queue.pop()</span><br><span class="line">        callback = weixin_request.callback</span><br><span class="line">        <span class="keyword">print</span>(<span class="string">'Schedule'</span>, weixin_request.url)</span><br><span class="line">        response = <span class="keyword">self</span>.request(weixin_request)</span><br><span class="line">        <span class="keyword">if</span> response <span class="keyword">and</span> response.status_code in VALID_STATUSES:</span><br><span class="line">            results = <span class="keyword">list</span>(callback(response))</span><br><span class="line">            <span class="keyword">if</span> results:</span><br><span class="line">                <span class="keyword">for</span> result in results:</span><br><span class="line">                    <span class="keyword">print</span>(<span class="string">'New Result'</span>, result)</span><br><span class="line">                    <span class="keyword">if</span> isinstance(result, WeixinRequest):</span><br><span class="line">                        <span class="keyword">self</span>.queue.add(result)</span><br><span class="line">                    <span class="keyword">if</span> isinstance(result, dict):</span><br><span class="line">                        <span class="keyword">self</span>.mysql.insert(<span class="string">'articles'</span>, result)</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                <span class="keyword">self</span>.error(weixin_request)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="keyword">self</span>.error(weixin_request)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里实现了一个 schedule() 方法，其内部是一个循环，循环的判断是队列不为空。 当队列不为空时，调用 pop() 方法取出下一个请求，调用 request() 方法执行这个请求，request() 方法的实现如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> requests import ReadTimeout, ConnectionError</span><br><span class="line"></span><br><span class="line">def request(self, weixin_request):</span><br><span class="line">    <span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">    执行请求</span></span><br><span class="line"><span class="string">    :param weixin_request: 请求</span></span><br><span class="line"><span class="string">    :return: 响应</span></span><br><span class="line"><span class="string">    "</span><span class="string">""</span></span><br><span class="line">    try:</span><br><span class="line">        <span class="keyword">if</span> weixin_request.need_proxy:</span><br><span class="line">           <span class="built_in"> proxy </span>= get_proxy()</span><br><span class="line">            <span class="keyword">if</span> proxy:</span><br><span class="line">                proxies = &#123;</span><br><span class="line">                    <span class="string">'http'</span>: <span class="string">'http://'</span> + proxy,</span><br><span class="line">                    <span class="string">'https'</span>: <span class="string">'https://'</span> + proxy</span><br><span class="line">                &#125;</span><br><span class="line">                return self.session.send(weixin_request.prepare(),</span><br><span class="line">                                         <span class="attribute">timeout</span>=weixin_request.timeout, <span class="attribute">allow_redirects</span>=<span class="literal">False</span>, <span class="attribute">proxies</span>=proxies)</span><br><span class="line">        return self.session.send(weixin_request.prepare(), <span class="attribute">timeout</span>=weixin_request.timeout, <span class="attribute">allow_redirects</span>=<span class="literal">False</span>)</span><br><span class="line">    except (ConnectionError, ReadTimeout) as e:</span><br><span class="line">        <span class="builtin-name">print</span>(e.args)</span><br><span class="line">        return <span class="literal">False</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里首先判断这个请求是否需要代理，如果需要代理，则调用 get_proxy() 方法获取代理，然后调用 Session 的 send() 方法执行这个请求。这里的请求调用了 prepare() 方法转化为 Prepared Request，具体的用法可以参考 <a href="http://docs.python-requests.org/en/master/user/advanced/#prepared-requests，同时设置" target="_blank" rel="noopener">http://docs.python-requests.org/en/master/user/advanced/#prepared-requests，同时设置</a> allow_redirects 为 False，timeout 是该请求的超时时间，最后响应返回。 执行 request() 方法之后会得到两种结果：一种是 False，即请求失败，连接错误；另一种是 Response 对象，还需要判断状态码，如果状态码合法，那么就进行解析，否则重新将请求加回队列。 如果状态码合法，解析的时候就会调用 WeixinRequest 的回调函数进行解析。比如这里的回调函数是 parse_index()，其实现如下：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> pyquery <span class="keyword">import</span> PyQuery <span class="keyword">as</span> pq</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">parse_index</span><span class="params">(self, response)</span>:</span></span><br><span class="line">    <span class="string">"""</span></span><br><span class="line"><span class="string">    解析索引页</span></span><br><span class="line"><span class="string">    :param response: 响应</span></span><br><span class="line"><span class="string">    :return: 新的响应</span></span><br><span class="line"><span class="string">    """</span></span><br><span class="line">    doc = pq(response.text)</span><br><span class="line">    items = doc(<span class="string">'.news-box .news-list li .txt-box h3 a'</span>).items()</span><br><span class="line">    <span class="keyword">for</span> item <span class="keyword">in</span> items:</span><br><span class="line">        url = item.attr(<span class="string">'href'</span>)</span><br><span class="line">        weixin_request = WeixinRequest(url=url, callback=self.parse_detail)</span><br><span class="line">        <span class="keyword">yield</span> weixin_request</span><br><span class="line">    next = doc(<span class="string">'#sogou_next'</span>).attr(<span class="string">'href'</span>)</span><br><span class="line">    <span class="keyword">if</span> next:</span><br><span class="line">        url = self.base_url + str(next)</span><br><span class="line">        weixin_request = WeixinRequest(url=url, callback=self.parse_index, need_proxy=<span class="literal">True</span>)</span><br><span class="line">        <span class="keyword">yield</span> weixin_request</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>此方法做了两件事：一件事就是获取本页的所有微信文章链接，另一件事就是获取下一页的链接，再构造成 WeixinRequest 之后 yield 返回。 然后，schedule() 方法将返回的结果进行遍历，利用 isinstance() 方法判断返回结果，如果返回结果是 WeixinRequest，就将其重新加入队列。 至此，第一次循环结束。 这时 while 循环会继续执行。队列已经包含第一页内容的文章详情页请求和下一页的请求，所以第二次循环得到的下一个请求就是文章详情页的请求，程序重新调用 request() 方法获取其响应，然后调用其对应的回调函数解析。这时详情页请求的回调方法就不同了，这次是 parse_detail() 方法，此方法实现如下：</p>
                  <figure class="highlight ceylon">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def parse<span class="number">_</span>detail(self, response):</span><br><span class="line">    <span class="string">"""</span></span><br><span class="line"><span class="string">    解析详情页</span></span><br><span class="line"><span class="string">    :param response: 响应</span></span><br><span class="line"><span class="string">    :return: 微信公众号文章</span></span><br><span class="line"><span class="string">    """</span></span><br><span class="line">    <span class="meta">doc</span> = pq(response.text)</span><br><span class="line">    data = &#123;<span class="string">'title'</span>: <span class="meta">doc</span>(<span class="string">'.rich_media_title'</span>).text(),</span><br><span class="line">        <span class="string">'content'</span>: <span class="meta">doc</span>(<span class="string">'.rich_media_content'</span>).text(),</span><br><span class="line">        <span class="string">'date'</span>: <span class="meta">doc</span>(<span class="string">'#post-date'</span>).text(),</span><br><span class="line">        <span class="string">'nickname'</span>: <span class="meta">doc</span>(<span class="string">'#js_profile_qrcode&gt; div &gt; strong'</span>).text(),</span><br><span class="line">        <span class="string">'wechat'</span>: <span class="meta">doc</span>(<span class="string">'#js_profile_qrcode&gt; div &gt; p:nth-child(3) &gt; span'</span>).text()&#125;</span><br><span class="line">    yield data</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这个方法解析了微信文章详情页的内容，提取出它的标题、正文文本、发布日期、发布人昵称、微信公众号名称，将这些信息组合成一个字典返回。 结果返回之后还需要判断类型，如是字典类型，程序就调用 mysql 对象的 insert() 方法将数据存入数据库。 这样，第二次循环执行完毕。 第三次循环、第四次循环，循环往复，每个请求都有各自的回调函数，索引页解析完毕之后会继续生成后续请求，详情页解析完毕之后会返回结果以便存储，直到爬取完毕。 现在，整个调度就完成了。 我们完善一下整个 Spider 代码，实现如下：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> requests <span class="keyword">import</span> Session</span><br><span class="line"><span class="keyword">from</span> config <span class="keyword">import</span> *</span><br><span class="line"><span class="keyword">from</span> db <span class="keyword">import</span> RedisQueue</span><br><span class="line"><span class="keyword">from</span> mysql <span class="keyword">import</span> MySQL</span><br><span class="line"><span class="keyword">from</span> request <span class="keyword">import</span> WeixinRequest</span><br><span class="line"><span class="keyword">from</span> urllib.parse <span class="keyword">import</span> urlencode</span><br><span class="line"><span class="keyword">import</span> requests</span><br><span class="line"><span class="keyword">from</span> pyquery <span class="keyword">import</span> PyQuery <span class="keyword">as</span> pq</span><br><span class="line"><span class="keyword">from</span> requests <span class="keyword">import</span> ReadTimeout, ConnectionError</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Spider</span><span class="params">()</span>:</span></span><br><span class="line">    base_url = <span class="string">'http://weixin.sogou.com/weixin'</span></span><br><span class="line">    keyword = <span class="string">'NBA'</span></span><br><span class="line">    headers = &#123;</span><br><span class="line">        <span class="string">'Accept'</span>: <span class="string">'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8'</span>,</span><br><span class="line">        <span class="string">'Accept-Encoding'</span>: <span class="string">'gzip, deflate'</span>,</span><br><span class="line">        <span class="string">'Accept-Language'</span>: <span class="string">'zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4,zh-TW;q=0.2,mt;q=0.2'</span>,</span><br><span class="line">        <span class="string">'Cache-Control'</span>: <span class="string">'max-age=0'</span>,</span><br><span class="line">        <span class="string">'Connection'</span>: <span class="string">'keep-alive'</span>,</span><br><span class="line">        <span class="string">'Cookie'</span>: <span class="string">'IPLOC=CN1100; SUID=6FEDCF3C541C940A000000005968CF55; SUV=1500041046435211; ABTEST=0|1500041048|v1; SNUID=CEA85AE02A2F7E6EAFF9C1FE2ABEBE6F; weixinIndexVisited=1; JSESSIONID=aaar_m7LEIW-jg_gikPZv; ld=Wkllllllll2BzGMVlllllVOo8cUlllll5G@HbZllll9lllllRklll5@@@@@@@@@@'</span>,</span><br><span class="line">        <span class="string">'Host'</span>: <span class="string">'weixin.sogou.com'</span>,</span><br><span class="line">        <span class="string">'Upgrade-Insecure-Requests'</span>: <span class="string">'1'</span>,</span><br><span class="line">        <span class="string">'User-Agent'</span>: <span class="string">'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36'</span></span><br><span class="line">    &#125;</span><br><span class="line">    session = Session()</span><br><span class="line">    queue = RedisQueue()</span><br><span class="line">    mysql = MySQL()</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">get_proxy</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        从代理池获取代理</span></span><br><span class="line"><span class="string">        :return:</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            response = requests.get(PROXY_POOL_URL)</span><br><span class="line">            <span class="keyword">if</span> response.status_code == <span class="number">200</span>:</span><br><span class="line">                print(<span class="string">'Get Proxy'</span>, response.text)</span><br><span class="line">                <span class="keyword">return</span> response.text</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line">        <span class="keyword">except</span> requests.ConnectionError:</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">start</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""初始化工作"""</span></span><br><span class="line">        <span class="comment"># 全局更新 Headers</span></span><br><span class="line">        self.session.headers.update(self.headers)</span><br><span class="line">        start_url = self.base_url + <span class="string">'?'</span> + urlencode(&#123;<span class="string">'query'</span>: self.keyword, <span class="string">'type'</span>: <span class="number">2</span>&#125;)</span><br><span class="line">        weixin_request = WeixinRequest(url=start_url, callback=self.parse_index, need_proxy=<span class="literal">True</span>)</span><br><span class="line">        <span class="comment"># 调度第一个请求</span></span><br><span class="line">        self.queue.add(weixin_request)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">parse_index</span><span class="params">(self, response)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        解析索引页</span></span><br><span class="line"><span class="string">        :param response: 响应</span></span><br><span class="line"><span class="string">        :return: 新的响应</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        doc = pq(response.text)</span><br><span class="line">        items = doc(<span class="string">'.news-box .news-list li .txt-box h3 a'</span>).items()</span><br><span class="line">        <span class="keyword">for</span> item <span class="keyword">in</span> items:</span><br><span class="line">            url = item.attr(<span class="string">'href'</span>)</span><br><span class="line">            weixin_request = WeixinRequest(url=url, callback=self.parse_detail)</span><br><span class="line">            <span class="keyword">yield</span> weixin_request</span><br><span class="line">        next = doc(<span class="string">'#sogou_next'</span>).attr(<span class="string">'href'</span>)</span><br><span class="line">        <span class="keyword">if</span> next:</span><br><span class="line">            url = self.base_url + str(next)</span><br><span class="line">            weixin_request = WeixinRequest(url=url, callback=self.parse_index, need_proxy=<span class="literal">True</span>)</span><br><span class="line">            <span class="keyword">yield</span> weixin_request</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">parse_detail</span><span class="params">(self, response)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        解析详情页</span></span><br><span class="line"><span class="string">        :param response: 响应</span></span><br><span class="line"><span class="string">        :return: 微信公众号文章</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        doc = pq(response.text)</span><br><span class="line">        data = &#123;<span class="string">'title'</span>: doc(<span class="string">'.rich_media_title'</span>).text(),</span><br><span class="line">            <span class="string">'content'</span>: doc(<span class="string">'.rich_media_content'</span>).text(),</span><br><span class="line">            <span class="string">'date'</span>: doc(<span class="string">'#post-date'</span>).text(),</span><br><span class="line">            <span class="string">'nickname'</span>: doc(<span class="string">'#js_profile_qrcode&gt; div &gt; strong'</span>).text(),</span><br><span class="line">            <span class="string">'wechat'</span>: doc(<span class="string">'#js_profile_qrcode&gt; div &gt; p:nth-child(3) &gt; span'</span>).text()&#125;</span><br><span class="line">        <span class="keyword">yield</span> data</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">request</span><span class="params">(self, weixin_request)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        执行请求</span></span><br><span class="line"><span class="string">        :param weixin_request: 请求</span></span><br><span class="line"><span class="string">        :return: 响应</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="keyword">if</span> weixin_request.need_proxy:</span><br><span class="line">                proxy = self.get_proxy()</span><br><span class="line">                <span class="keyword">if</span> proxy:</span><br><span class="line">                    proxies = &#123;</span><br><span class="line">                        <span class="string">'http'</span>: <span class="string">'http://'</span> + proxy,</span><br><span class="line">                        <span class="string">'https'</span>: <span class="string">'https://'</span> + proxy</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="keyword">return</span> self.session.send(weixin_request.prepare(),</span><br><span class="line">                                             timeout=weixin_request.timeout, allow_redirects=<span class="literal">False</span>, proxies=proxies)</span><br><span class="line">            <span class="keyword">return</span> self.session.send(weixin_request.prepare(), timeout=weixin_request.timeout, allow_redirects=<span class="literal">False</span>)</span><br><span class="line">        <span class="keyword">except</span> (ConnectionError, ReadTimeout) <span class="keyword">as</span> e:</span><br><span class="line">            print(e.args)</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">error</span><span class="params">(self, weixin_request)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        错误处理</span></span><br><span class="line"><span class="string">        :param weixin_request: 请求</span></span><br><span class="line"><span class="string">        :return:</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        weixin_request.fail_time = weixin_request.fail_time + <span class="number">1</span></span><br><span class="line">        print(<span class="string">'Request Failed'</span>, weixin_request.fail_time, <span class="string">'Times'</span>, weixin_request.url)</span><br><span class="line">        <span class="keyword">if</span> weixin_request.fail_time &lt; MAX_FAILED_TIME:</span><br><span class="line">            self.queue.add(weixin_request)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">schedule</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        调度请求</span></span><br><span class="line"><span class="string">        :return:</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">while</span> <span class="keyword">not</span> self.queue.empty():</span><br><span class="line">            weixin_request = self.queue.pop()</span><br><span class="line">            callback = weixin_request.callback</span><br><span class="line">            print(<span class="string">'Schedule'</span>, weixin_request.url)</span><br><span class="line">            response = self.request(weixin_request)</span><br><span class="line">            <span class="keyword">if</span> response <span class="keyword">and</span> response.status_code <span class="keyword">in</span> VALID_STATUSES:</span><br><span class="line">                results = list(callback(response))</span><br><span class="line">                <span class="keyword">if</span> results:</span><br><span class="line">                    <span class="keyword">for</span> result <span class="keyword">in</span> results:</span><br><span class="line">                        print(<span class="string">'New Result'</span>, result)</span><br><span class="line">                        <span class="keyword">if</span> isinstance(result, WeixinRequest):</span><br><span class="line">                            self.queue.add(result)</span><br><span class="line">                        <span class="keyword">if</span> isinstance(result, dict):</span><br><span class="line">                            self.mysql.insert(<span class="string">'articles'</span>, result)</span><br><span class="line">                <span class="keyword">else</span>:</span><br><span class="line">                    self.error(weixin_request)</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                self.error(weixin_request)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">run</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        入口</span></span><br><span class="line"><span class="string">        :return:</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        self.start()</span><br><span class="line">        self.schedule()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line">    spider = Spider()</span><br><span class="line">    spider.run()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>最后，我们加了一个 run() 方法作为入口，启动的时候只需要执行 Spider 的 run() 方法即可。</p>
                  <h3 id="9-MySQL-存储"><a href="#9-MySQL-存储" class="headerlink" title="9. MySQL 存储"></a>9. MySQL 存储</h3>
                  <p>整个调度模块完成了，上面还没提及到的就是存储模块，在这里还需要定义一个 MySQL 类供存储数据，实现如下：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">REDIS_HOST = <span class="string">'localhost'</span></span><br><span class="line">REDIS_PORT = <span class="number">6379</span></span><br><span class="line">REDIS_PASSWORD = <span class="string">'foobared'</span></span><br><span class="line">REDIS_KEY = <span class="string">'weixin'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> pymysql</span><br><span class="line"><span class="keyword">from</span> config <span class="keyword">import</span> *</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MySQL</span><span class="params">()</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, host=MYSQL_HOST, username=MYSQL_USER, password=MYSQL_PASSWORD, port=MYSQL_PORT,</span></span></span><br><span class="line"><span class="function"><span class="params">                 database=MYSQL_DATABASE)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        MySQL 初始化</span></span><br><span class="line"><span class="string">        :param host:</span></span><br><span class="line"><span class="string">        :param username:</span></span><br><span class="line"><span class="string">        :param password:</span></span><br><span class="line"><span class="string">        :param port:</span></span><br><span class="line"><span class="string">        :param database:</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            self.db = pymysql.connect(host, username, password, database, charset=<span class="string">'utf8'</span>, port=port)</span><br><span class="line">            self.cursor = self.db.cursor()</span><br><span class="line">        <span class="keyword">except</span> pymysql.MySQLError <span class="keyword">as</span> e:</span><br><span class="line">            print(e.args)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">insert</span><span class="params">(self, table, data)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        插入数据</span></span><br><span class="line"><span class="string">        :param table:</span></span><br><span class="line"><span class="string">        :param data:</span></span><br><span class="line"><span class="string">        :return:</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        keys = <span class="string">', '</span>.join(data.keys())</span><br><span class="line">        values = <span class="string">', '</span>.join([<span class="string">'% s'</span>] * len(data))</span><br><span class="line">        sql_query = <span class="string">'insert into % s (% s) values (% s)'</span> % (table, keys, values)</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            self.cursor.execute(sql_query, tuple(data.values()))</span><br><span class="line">            self.db.commit()</span><br><span class="line">        <span class="keyword">except</span> pymysql.MySQLError <span class="keyword">as</span> e:</span><br><span class="line">            print(e.args)</span><br><span class="line">            self.db.rollback()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p><strong>init</strong>() 方法初始化了 MySQL 连接，需要 MySQL 的用户、密码、端口、数据库名等信息。数据库名为 weixin，需要自己创建。 insert() 方法传入表名和字典即可动态构造 SQL，在 5.2 节中也有讲到，SQL 构造之后执行即可插入数据。 我们还需要提前建立一个数据表，表名为 articles，建表的 SQL 语句如下：</p>
                  <figure class="highlight sql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> <span class="string">`articles`</span> (<span class="string">`id`</span> <span class="built_in">int</span>(<span class="number">11</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span>,</span><br><span class="line">  <span class="string">`title`</span> <span class="built_in">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span>,</span><br><span class="line">  <span class="string">`content`</span> <span class="built_in">text</span> <span class="keyword">NOT</span> <span class="literal">NULL</span>,</span><br><span class="line">  <span class="string">`date`</span> <span class="built_in">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span>,</span><br><span class="line">  <span class="string">`wechat`</span> <span class="built_in">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span>,</span><br><span class="line">  <span class="string">`nickname`</span> <span class="built_in">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span></span><br><span class="line">) <span class="keyword">DEFAULT</span> <span class="keyword">CHARSET</span>=utf8;</span><br><span class="line"><span class="keyword">ALTER</span> <span class="keyword">TABLE</span> <span class="string">`articles`</span> <span class="keyword">ADD</span> PRIMARY <span class="keyword">KEY</span> (<span class="string">`id`</span>);</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>现在，我们的整个爬虫就算完成了。</p>
                  <h3 id="10-运行"><a href="#10-运行" class="headerlink" title="10. 运行"></a>10. 运行</h3>
                  <p>示例运行结果如图 9-29 所示： <img src="https://qiniu.cuiqingcai.com/2019-10-20-060111.jpg" alt=""> 图 9-29 运行结果 程序首先调度了第一页结果对应的请求，获取了代理执行此请求，随后得到了 11 个新请求，请求都是 WeixinRequest 类型，将其再加入队列。随后继续调度新加入的请求，也就是文章详情页对应的请求，再执行，得到的就是文章详情对应的提取结果，提取结果是字典类型。 程序循环往复，不断爬取，直至所有结果爬取完毕，程序终止，爬取完成。 爬取结果如图 9-30 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-060114.jpg" alt=""> 图 9-30 爬取结果 我们可以看到，相关微信文章都已被存储到数据库里了。</p>
                  <h3 id="11-本节代码"><a href="#11-本节代码" class="headerlink" title="11. 本节代码"></a>11. 本节代码</h3>
                  <p>本节代码地址为：<a href="https://github.com/Python3WebSpider/Weixin" target="_blank" rel="noopener">https://github.com/Python3WebSpider/Weixin</a>，运行之前请先配置好代理池。</p>
                  <h3 id="12-结语"><a href="#12-结语" class="headerlink" title="12. 结语"></a>12. 结语</h3>
                  <p>以上内容便是使用代理爬取微信公众号文章的方法，涉及的新知识点不少，希望大家可以好好消化。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-10-20 14:15:26" itemprop="dateCreated datePublished" datetime="2019-10-20T14:15:26+08:00">2019-10-20</time>
                </span>
                <span id="/7844.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 9.5-使用代理爬取微信公众号文章" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>16k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>14 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/7842.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/7842.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 9.4-ADSL 拨号代理</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>我们尝试维护过一个代理池。代理池可以挑选出许多可用代理，但是常常其稳定性不高、响应速度慢，而且这些代理通常是公共代理，可能不止一人同时使用，其 IP 被封的概率很大。另外，这些代理可能有效时间比较短，虽然代理池一直在筛选，但如果没有及时更新状态，也有可能获取到不可用的代理。 如果要追求更加稳定的代理，就需要购买专有代理或者自己搭建代理服务器。但是服务器一般都是固定的 IP，我们总不能搭建 100 个代理就用 100 台服务器吧，这显然是不现实的。 所以，ADSL 动态拨号主机就派上用场了。下面我们来了解一下 ADSL 拨号代理服务器的相关设置。</p>
                  <h3 id="1-什么是-ADSL"><a href="#1-什么是-ADSL" class="headerlink" title="1. 什么是 ADSL"></a>1. 什么是 ADSL</h3>
                  <p>ADSL（Asymmetric Digital Subscriber Line，非对称数字用户环路），它的上行和下行带宽不对称，它采用频分复用技术把普通的电话线分成了电话、上行和下行 3 个相对独立的信道，从而避免了相互之间的干扰。 ADSL 通过拨号的方式上网，需要输入 ADSL 账号和密码，每次拨号就更换一个 IP。IP 分布在多个 A 段，如果 IP 都能使用，则意味着 IP 量级可达千万。如果我们将 ADSL 主机作为代理，每隔一段时间主机拨号就换一个 IP，这样可以有效防止 IP 被封禁。另外，主机的稳定性很好，代理响应速度很快。</p>
                  <h3 id="2-准备工作"><a href="#2-准备工作" class="headerlink" title="2. 准备工作"></a>2. 准备工作</h3>
                  <p>首先需要成功安装 Redis 数据库并启动服务，另外还需要安装 requests、redis-py、Tornado 库。如果没有安装，读者可以参考第一章的安装说明。</p>
                  <h3 id="3-购买主机"><a href="#3-购买主机" class="headerlink" title="3. 购买主机"></a>3. 购买主机</h3>
                  <p>我们先购买一台动态拨号 VPS 主机，这样的主机服务商相当多。在这里使用了云立方，官方网站：<a href="http://www.yunlifang.cn/dynamicvps.asp" target="_blank" rel="noopener">http://www.yunlifang.cn/dynamicvps.asp</a>。 建议选择电信线路。可以自行选择主机配置，主要考虑带宽是否满足需求。 然后进入拨号主机的后台，预装一个操作系统，如图 9-10 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-052822.jpg" alt=""> 图 9-10 预装操作系统 推荐安装 CentOS 7 系统。 然后找到远程管理面板远程连接的用户名和密码，也就是 SSH 远程连接服务器的信息。比如我使用的 IP 和端口是 153.36.65.214:20063，用户名是 root。命令行下输入如下内容：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">ssh <span class="symbol">root@</span><span class="number">153.36</span><span class="number">.65</span><span class="number">.214</span> -p <span class="number">20063</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>输入管理密码，就可以连接上远程服务器了。 进入之后，我们发现一个可用的脚本文件 ppp.sh，这是拨号初始化的脚本。运行此脚本会提示输入拨号的用户名和密码，然后它就开始各种拨号配置。一次配置成功，后面拨号就不需要重复输入用户名和密码。 运行 ppp.sh 脚本，输入用户名、密码等待它的配置完成，如图 9-11 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-052827.jpg" alt=""> 图 9-11 配置页面 提示成功之后就可以进行拨号了。注意，在拨号之前测试 ping 任何网站都是不通的，因为当前网络还没联通。输入如下拨号命令：</p>
                  <figure class="highlight crmsh">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">adsl-<span class="literal">start</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>拨号命令成功运行，没有报错信息，耗时约几秒。接下来再去 ping 外网就可以通了。 如果要停止拨号，可以输入如下命令：</p>
                  <figure class="highlight arduino">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">adsl-<span class="built_in">stop</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>之后，可以发现又连不通网络了，如图 9-12 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-052841.jpg" alt=""> 图 9-12 拨号建立连接 断线重播的命令就是二者组合起来，先执行 adsl-stop，再执行 adsl-start。每次拨号，ifconfig 命令观察主机的 IP，发现主机的 IP 一直在变化，网卡名称叫作 ppp0，如图 9-13 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-052851.jpg" alt=""> 图 9-13 网络设备信息 接下来，我们要做两件事：一是怎样将主机设置为代理服务器，二是怎样实时获取拨号主机的 IP。</p>
                  <h3 id="4-设置代理服务器"><a href="#4-设置代理服务器" class="headerlink" title="4. 设置代理服务器"></a>4. 设置代理服务器</h3>
                  <p>在 Linux 下搭建 HTTP 代理服务器，推荐 TinyProxy 和 Squid，配置都非常简单。在这里我们以 TinyProxy 为例来讲解一下怎样搭建代理服务器。</p>
                  <h4 id="安装-TinyProxy"><a href="#安装-TinyProxy" class="headerlink" title="安装 TinyProxy"></a>安装 TinyProxy</h4>
                  <p>第一步就是安装 TinyProxy 软件。在这里我使用的系统是 CentOS，所以使用 yum 来安装。如果是其他系统，如 Ubuntu，可以选择 apt-get 等命令安装。 命令行执行 yum 安装指令：</p>
                  <figure class="highlight sql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">yum <span class="keyword">install</span> -y epel-<span class="keyword">release</span></span><br><span class="line">yum <span class="keyword">update</span> -y</span><br><span class="line">yum <span class="keyword">install</span> -y tinyproxy</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行完成之后就可以完成 tinyproxy 的安装了。</p>
                  <h4 id="配置-TinyProxy"><a href="#配置-TinyProxy" class="headerlink" title="配置 TinyProxy"></a>配置 TinyProxy</h4>
                  <p>TinyProxy 安装完成之后还要配置一下才可以用作代理服务器。我们需要编辑配置文件，此文件一般的路径是 /etc/tinyproxy/tinyproxy.conf。 可以看到有一行</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">Port <span class="number">8888</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里可以设置代理的端口，默认是 8888。 继续向下找到如下代码：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">Allow <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这行代码表示被允许连接的主机 IP。如果希望连接任何主机，那就直接将这行代码注释即可。在这里我们选择直接注释，也就是任何主机都可以使用这台主机作为代理服务器。 修改为如下代码：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"># Allow <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>设置完成之后重启 TinyProxy 即可：</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="selector-tag">systemctl</span> <span class="selector-tag">enable</span> <span class="selector-tag">tinyproxy</span><span class="selector-class">.service</span></span><br><span class="line"><span class="selector-tag">systemctl</span> <span class="selector-tag">restart</span> <span class="selector-tag">tinyproxy</span><span class="selector-class">.service</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>防火墙开放该端口：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">iptables -I <span class="keyword">INPUT</span> -p tcp <span class="comment">--dport 8888 -j ACCEPT</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>当然如果想直接关闭防火墙也可以：</p>
                  <figure class="highlight arduino">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">systemctl <span class="built_in">stop</span> firewalld.service</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我们就完成了 TinyProxy 的配置了。</p>
                  <h4 id="验证-TinyProxy"><a href="#验证-TinyProxy" class="headerlink" title="验证 TinyProxy"></a>验证 TinyProxy</h4>
                  <p>首先，用 ifconfig 查看当前主机的 IP。比如，当前我的主机拨号 IP 为 112.84.118.216，在其他的主机运行测试一下。 用 curl 命令设置代理请求 httpbin，检测代理是否生效。</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">curl -x <span class="number">112.84</span><span class="number">.118</span><span class="number">.216</span>:<span class="number">8888</span> httpbin.org/<span class="keyword">get</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果如图 9-14 所示： <img src="https://qiniu.cuiqingcai.com/2019-10-20-052902.jpg" alt=""> 图 9-14 运行结果 如果有正常的结果输出，并且 origin 的值为代理 IP 的地址，就证明 TinyProxy 配置成功了。</p>
                  <h3 id="5-动态获取-IP"><a href="#5-动态获取-IP" class="headerlink" title="5. 动态获取 IP"></a>5. 动态获取 IP</h3>
                  <p>现在可以执行命令让主机动态切换 IP，也在主机上搭建了代理服务器。我们只需要知道拨号后的 IP 就可以使用代理。 我们考虑到，在一台主机拨号切换 IP 的间隙代理是不可用的，在这拨号的几秒时间内如果有第二台主机顶替第一台主机，那就可以解决拨号间隙代理无法使用的问题了。所以我们要设计的架构必须要考虑支持多主机的问题。 假如有 10 台拨号主机同时需要维护，而爬虫需要使用这 10 台主机的代理，那么在爬虫端维护的开销是非常大的。如果爬虫在不同的机器上运行，那么每个爬虫必须要获得这 10 台拨号主机的配置，这显然是不理想的。 为了更加方便地使用代理，我们可以像上文的代理池一样定义一个统一的代理接口，爬虫端只需要配置代理接口即可获取可用代理。要搭建一个接口，就势必需要一台服务器，而接口的数据从哪里获得呢，当然最理想的还是选择数据库。 比如我们需要同时维护 10 台拨号主机，每台拨号主机都会定时拨号，那这样每台主机在某个时刻可用的代理只有一个，所以我们没有必要存储之前的拨号代理，因为重新拨号之后之前的代理已经不能用了，所以只需要将之前的代理更新其内容就好了。数据库要做的就是定时对每台主机的代理进行更新，而更新时又需要拨号主机的唯一标识，根据主机标识查出这条数据，然后将这条数据对应的代理更新。 所以数据库端就需要存储一个主机标识到代理的映射关系。那么很自然地我们就会想到关系型数据库，如 MySQL 或者 Redis 的 Hash 存储，只需存储一个映射关系，不需要很多字段，而且 Redis 比 MySQL 效率更高、使用更方便，所以最终选定的存储方式就是 Redis 的 Hash。</p>
                  <h3 id="6-存储模块"><a href="#6-存储模块" class="headerlink" title="6. 存储模块"></a>6. 存储模块</h3>
                  <p>那么接下来我们要做可被远程访问的 Redis 数据库，各个拨号机器只需要将各自的主机标识和当前 IP 和端口（也就是代理）发送给数据库就好了。 先定义一个操作 Redis 数据库的类，示例如下：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">import</span> redis</span><br><span class="line"><span class="keyword">import</span> random</span><br><span class="line"></span><br><span class="line"><span class="comment"># Redis 数据库 IP</span></span><br><span class="line">REDIS_HOST = <span class="string">'remoteaddress'</span></span><br><span class="line"><span class="comment"># Redis 数据库密码，如无则填 None</span></span><br><span class="line">REDIS_PASSWORD = <span class="string">'foobared'</span></span><br><span class="line"><span class="comment"># Redis 数据库端口</span></span><br><span class="line">REDIS_PORT = <span class="number">6379</span></span><br><span class="line"><span class="comment"># 代理池键名</span></span><br><span class="line">PROXY_KEY = <span class="string">'adsl'</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">RedisClient</span><span class="params">(object)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, proxy_key=PROXY_KEY)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        初始化 Redis 连接</span></span><br><span class="line"><span class="string">        :param host: Redis 地址</span></span><br><span class="line"><span class="string">        :param port: Redis 端口</span></span><br><span class="line"><span class="string">        :param password: Redis 密码</span></span><br><span class="line"><span class="string">        :param proxy_key: Redis 哈希表名</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=<span class="literal">True</span>)</span><br><span class="line">        self.proxy_key = proxy_key</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">set</span><span class="params">(self, name, proxy)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        设置代理</span></span><br><span class="line"><span class="string">        :param name: 主机名称</span></span><br><span class="line"><span class="string">        :param proxy: 代理</span></span><br><span class="line"><span class="string">        :return: 设置结果</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> self.db.hset(self.proxy_key, name, proxy)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">get</span><span class="params">(self, name)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        获取代理</span></span><br><span class="line"><span class="string">        :param name: 主机名称</span></span><br><span class="line"><span class="string">        :return: 代理</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> self.db.hget(self.proxy_key, name)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">count</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        获取代理总数</span></span><br><span class="line"><span class="string">        :return: 代理总数</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> self.db.hlen(self.proxy_key)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">remove</span><span class="params">(self, name)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        删除代理</span></span><br><span class="line"><span class="string">        :param name: 主机名称</span></span><br><span class="line"><span class="string">        :return: 删除结果</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> self.db.hdel(self.proxy_key, name)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">names</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        获取主机名称列表</span></span><br><span class="line"><span class="string">        :return: 获取主机名称列表</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> self.db.hkeys(self.proxy_key)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">proxies</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        获取代理列表</span></span><br><span class="line"><span class="string">        :return: 代理列表</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        <span class="keyword">return</span> self.db.hvals(self.proxy_key)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">random</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        随机获取代理</span></span><br><span class="line"><span class="string">        :return:</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        proxies = self.proxies()</span><br><span class="line">        <span class="keyword">return</span> random.choice(proxies)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">all</span><span class="params">(self)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        获取字典</span></span><br><span class="line"><span class="string">        :return:</span></span><br><span class="line"><span class="string">        """</span><span class="keyword">return</span> self.db.hgetall(self.proxy_key)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里定义了一个 RedisClient 类，在<strong>init</strong>() 方法中初始化了 Redis 连接，其中 REDIS_HOST 就是远程 Redis 的地址，REDIS_PASSWORD 是密码，REDIS_PORT 是端口，PROXY_KEY 是存储代理的散列表的键名。 接下来定义了一个 set() 方法，这个方法用来向散列表添加映射关系。映射是从主机标识到代理的映射，比如一台主机的标识为 adsl1，当前的代理为 118.119.111.172:8888，那么散列表中就会存储一个 key 为 adsl1、value 为 118.119.111.172:8888 的映射，Hash 结构如图 9-15 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-052948.jpg" alt=""> 图 9-15 Hash 结构 如果有多台主机，只需要向 Hash 中添加映射即可。 另外，get() 方法就是从散列表中取出某台主机对应的代理。remove() 方法则是从散列表中移除对应的主机的代理。还有 names()、proxies()、all() 方法则是分别获取散列表中的主机列表、代理列表及所有主机代理映射。count() 方法则是返回当前散列表的大小，也就是可用代理的数目。 最后还有一个比较重要的方法 random()，它随机从散列表中取出一个可用代理，类似前面代理池的思想，确保每个代理都能被取到。 如果要对数据库进行操作，只需要初始化 RedisClient 对象，然后调用它的 set() 或者 remove() 方法，即可对散列表进行设置和删除。</p>
                  <h3 id="7-拨号模块"><a href="#7-拨号模块" class="headerlink" title="7. 拨号模块"></a>7. 拨号模块</h3>
                  <p>接下来要做的就是拨号，并把新的 IP 保存到 Redis 散列表里。 首先是拨号定时，它分为定时拨号和非定时拨号两种选择。 非定时拨号：最好的方法就是向该主机发送一个信号，然后主机就启动拨号，但这样做的话，我们首先要搭建一个重新拨号的接口，如搭建一个 Web 接口，请求该接口即进行拨号，但开始拨号之后，此时主机的状态就从在线转为离线，而此时的 Web 接口也就相应失效了，拨号过程无法再连接，拨号之后接口的 IP 也变了，所以我们无法通过接口来方便地控制拨号过程和获取拨号结果，下次拨号还得改变拨号请求接口，所以非定时拨号的开销还是比较大的。 定时拨号：我们只需要在拨号主机上运行定时脚本即可，每隔一段时间拨号一次，更新 IP，然后将 IP 在 Redis 散列表中更新即可，非常简单易用，另外可以适当将拨号频率调高一点，减少短时间内 IP 被封的可能性。 在这里选择定时拨号。 接下来就是获取 IP。获取拨号后的 IP 非常简单，只需要调用 ifconfig 命令，然后解析出对应网卡的 IP 即可。 获取了 IP 之后，我们还需要进行有效性检测。拨号主机可以自己检测，比如可以利用 requests 设置自身的代理请求外网，如果成功，那么证明代理可用，然后再修改 Redis 散列表，更新代理。 需要注意，由于在拨号的间隙拨号主机是离线状态，而此时 Redis 散列表中还存留了上次的代理，一旦这个代理被取用了，该代理是无法使用的。为了避免这个情况，每台主机在拨号之前还需要将自身的代理从 Redis 散列表中移除。 这样基本的流程就理顺了，我们用如下代码实现：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">import re</span><br><span class="line">import time</span><br><span class="line">import requests</span><br><span class="line"><span class="keyword">from</span> requests.exceptions import ConnectionError, ReadTimeout</span><br><span class="line"><span class="keyword">from</span> db import RedisClient</span><br><span class="line"></span><br><span class="line"><span class="comment"># 拨号网卡</span></span><br><span class="line">ADSL_IFNAME = <span class="string">'ppp0'</span></span><br><span class="line"><span class="comment"># 测试 URL</span></span><br><span class="line">TEST_URL = <span class="string">'http://www.baidu.com'</span></span><br><span class="line"><span class="comment"># 测试超时时间</span></span><br><span class="line">TEST_TIMEOUT = 20</span><br><span class="line"><span class="comment"># 拨号间隔</span></span><br><span class="line">ADSL_CYCLE = 100</span><br><span class="line"><span class="comment"># 拨号出错重试间隔</span></span><br><span class="line">ADSL_ERROR_CYCLE = 5</span><br><span class="line"><span class="comment"># ADSL 命令</span></span><br><span class="line">ADSL_BASH = <span class="string">'adsl-stop;adsl-start'</span></span><br><span class="line"><span class="comment"># 代理运行端口</span></span><br><span class="line">PROXY_PORT = 8888</span><br><span class="line"><span class="comment"># 客户端唯一标识</span></span><br><span class="line">CLIENT_NAME = <span class="string">'adsl1'</span></span><br><span class="line"></span><br><span class="line">class Sender():</span><br><span class="line">    def get_ip(self, <span class="attribute">ifname</span>=ADSL_IFNAME):</span><br><span class="line">        <span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">        获取本机 IP</span></span><br><span class="line"><span class="string">        :param ifname: 网卡名称</span></span><br><span class="line"><span class="string">        :return:</span></span><br><span class="line"><span class="string">        "</span><span class="string">""</span></span><br><span class="line">        (status, output) = subprocess.getstatusoutput(<span class="string">'ifconfig'</span>)</span><br><span class="line">        <span class="keyword">if</span> status == 0:</span><br><span class="line">            pattern = re.compile(ifname + <span class="string">'.*?inet.*?(d+.d+.d+.d+).*?netmask'</span>, re.S)</span><br><span class="line">            result = re.search(pattern, output)</span><br><span class="line">            <span class="keyword">if</span> result:</span><br><span class="line">               <span class="built_in"> ip </span>= result.group(1)</span><br><span class="line">                return ip</span><br><span class="line"></span><br><span class="line">    def test_proxy(self, proxy):</span><br><span class="line">        <span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">        测试代理</span></span><br><span class="line"><span class="string">        :param proxy: 代理</span></span><br><span class="line"><span class="string">        :return: 测试结果</span></span><br><span class="line"><span class="string">        "</span><span class="string">""</span></span><br><span class="line">        try:</span><br><span class="line">            response = requests.<span class="builtin-name">get</span>(TEST_URL, proxies=&#123;</span><br><span class="line">                <span class="string">'http'</span>: <span class="string">'http://'</span> + proxy,</span><br><span class="line">                <span class="string">'https'</span>: <span class="string">'https://'</span> + proxy</span><br><span class="line">            &#125;, <span class="attribute">timeout</span>=TEST_TIMEOUT)</span><br><span class="line">            <span class="keyword">if</span> response.status_code == 200:</span><br><span class="line">                return <span class="literal">True</span></span><br><span class="line">        except (ConnectionError, ReadTimeout):</span><br><span class="line">            return <span class="literal">False</span></span><br><span class="line"></span><br><span class="line">    def remove_proxy(self):</span><br><span class="line">        <span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">        移除代理</span></span><br><span class="line"><span class="string">        :return: None</span></span><br><span class="line"><span class="string">        "</span><span class="string">""</span></span><br><span class="line">        self.redis = RedisClient()</span><br><span class="line">        self.redis.<span class="builtin-name">remove</span>(CLIENT_NAME)</span><br><span class="line">        <span class="builtin-name">print</span>(<span class="string">'Successfully Removed Proxy'</span>)</span><br><span class="line"></span><br><span class="line">    def set_proxy(self, proxy):</span><br><span class="line">        <span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">        设置代理</span></span><br><span class="line"><span class="string">        :param proxy: 代理</span></span><br><span class="line"><span class="string">        :return: None</span></span><br><span class="line"><span class="string">        "</span><span class="string">""</span></span><br><span class="line">        self.redis = RedisClient()</span><br><span class="line">        <span class="keyword">if</span> self.redis.<span class="builtin-name">set</span>(CLIENT_NAME, proxy):</span><br><span class="line">            <span class="builtin-name">print</span>(<span class="string">'Successfully Set Proxy'</span>, proxy)</span><br><span class="line"></span><br><span class="line">    def adsl(self):</span><br><span class="line">        <span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">        拨号主进程</span></span><br><span class="line"><span class="string">        :return: None</span></span><br><span class="line"><span class="string">        "</span><span class="string">""</span></span><br><span class="line">        <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">            <span class="builtin-name">print</span>(<span class="string">'ADSL Start, Remove Proxy, Please wait'</span>)</span><br><span class="line">            self.remove_proxy()</span><br><span class="line">            (status, output) = subprocess.getstatusoutput(ADSL_BASH)</span><br><span class="line">            <span class="keyword">if</span> status == 0:</span><br><span class="line">                <span class="builtin-name">print</span>(<span class="string">'ADSL Successfully'</span>)</span><br><span class="line">               <span class="built_in"> ip </span>= self.get_ip()</span><br><span class="line">                <span class="keyword">if</span> ip:</span><br><span class="line">                    <span class="builtin-name">print</span>(<span class="string">'Now IP'</span>, ip)</span><br><span class="line">                    <span class="builtin-name">print</span>(<span class="string">'Testing Proxy, Please Wait'</span>)</span><br><span class="line">                   <span class="built_in"> proxy </span>= <span class="string">'&#123;ip&#125;:&#123;port&#125;'</span>.format(<span class="attribute">ip</span>=ip, <span class="attribute">port</span>=PROXY_PORT)</span><br><span class="line">                    <span class="keyword">if</span> self.test_proxy(proxy):</span><br><span class="line">                        <span class="builtin-name">print</span>(<span class="string">'Valid Proxy'</span>)</span><br><span class="line">                        self.set_proxy(proxy)</span><br><span class="line">                        <span class="builtin-name">print</span>(<span class="string">'Sleeping'</span>)</span><br><span class="line">                        time.sleep(ADSL_CYCLE)</span><br><span class="line">                    <span class="keyword">else</span>:</span><br><span class="line">                        <span class="builtin-name">print</span>(<span class="string">'Invalid Proxy'</span>)</span><br><span class="line">                <span class="keyword">else</span>:</span><br><span class="line">                    <span class="builtin-name">print</span>(<span class="string">'Get IP Failed, Re Dialing'</span>)</span><br><span class="line">                    time.sleep(ADSL_ERROR_CYCLE)</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                <span class="builtin-name">print</span>(<span class="string">'ADSL Failed, Please Check'</span>)</span><br><span class="line">                time.sleep(ADSL_ERROR_CYCLE)</span><br><span class="line">def <span class="builtin-name">run</span>():</span><br><span class="line">    sender = Sender()</span><br><span class="line">    sender.adsl()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里定义了一个 Sender 类，它的主要作用是执行定时拨号，并将新的 IP 测试通过之后更新到远程 Redis 散列表里。 主方法是 adsl() 方法，它首先是一个无限循环，循环体内就是拨号的逻辑。 adsl() 方法首先调用了 remove_proxy() 方法，将远程 Redis 散列表中本机对应的代理移除，避免拨号时本主机的残留代理被取到。 接下来利用 subprocess 模块来执行拨号脚本，拨号脚本很简单，就是 stop 之后再 start，这里将拨号的命令直接定义成了 ADSL_BASH。 随后程序又调用 get_ip() 方法，通过 subprocess 模块执行获取 IP 的命令 ifconfig，然后根据网卡名称获取了当前拨号网卡的 IP 地址，即拨号后的 IP。 再接下来就需要测试代理有效性了。程序首先调用了 test_proxy() 方法，将自身的代理设置好，使用 requests 库来用代理连接 TEST_URL。在此 TEST_URL 设置为百度，如果请求成功，则证明代理有效。 如果代理有效，再调用 set_proxy() 方法将 Redis 散列表中本机对应的代理更新，设置时需要指定本机唯一标识和本机当前代理。本机唯一标识可随意配置，其对应的变量为 CLIENT_NAME，保证各台拨号主机不冲突即可。本机当前代理则由拨号后的新 IP 加端口组合而成。通过调用 RedisClient 的 set() 方法，参数 name 为本机唯一标识，proxy 为拨号后的新代理，执行之后便可以更新散列表中的本机代理了。 建议至少配置两台主机，这样在一台主机的拨号间隙还有另一台主机的代理可用。拨号主机的数量不限，越多越好。 在拨号主机上执行拨号脚本，示例输出如图 9-16 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-052912.jpg" alt=""> 图 9-16 示例输出 首先移除了代理，再进行拨号，拨号完成之后获取新的 IP，代理检测成功之后就设置到 Redis 散列表中，然后等待一段时间再重新进行拨号。 我们添加了多台拨号主机，这样就有多个稳定的定时更新的代理可用了。Redis 散列表会实时更新各台拨号主机的代理，如图 9-17 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-053042.jpg" alt=""> 图 9-17 Hash 结构 图中所示是四台 ADSL 拨号主机配置并运行后的散列表的内容，表中的代理都是可用的。</p>
                  <h3 id="8-接口模块"><a href="#8-接口模块" class="headerlink" title="8. 接口模块"></a>8. 接口模块</h3>
                  <p>目前为止，我们已经成功实时更新拨号主机的代理。不过还缺少一个模块，那就是接口模块。像之前的代理池一样，我们也定义一些接口来获取代理，如 random 获取随机代理、count 获取代理个数等。 我们选用 Tornado 来实现，利用 Tornado 的 Server 模块搭建 Web 接口服务，示例如下：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> tornado.ioloop</span><br><span class="line"><span class="keyword">import</span> tornado.web</span><br><span class="line"><span class="keyword">from</span> tornado.web <span class="keyword">import</span> RequestHandler, Application</span><br><span class="line"></span><br><span class="line"><span class="comment"># API 端口</span></span><br><span class="line">API_PORT = <span class="number">8000</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MainHandler</span><span class="params">(RequestHandler)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">initialize</span><span class="params">(self, redis)</span>:</span></span><br><span class="line">        self.redis = redis</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">get</span><span class="params">(self, api=<span class="string">''</span>)</span>:</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> api:</span><br><span class="line">            links = [<span class="string">'random'</span>, <span class="string">'proxies'</span>, <span class="string">'names'</span>, <span class="string">'all'</span>, <span class="string">'count'</span>]</span><br><span class="line">            self.write(<span class="string">'&lt;h4&gt;Welcome to ADSL Proxy API&lt;/h4&gt;'</span>)</span><br><span class="line">            <span class="keyword">for</span> link <span class="keyword">in</span> links:</span><br><span class="line">                self.write(<span class="string">'&lt;a href='</span> + link + <span class="string">'&gt;'</span> + link + <span class="string">'&lt;/a&gt;&lt;br&gt;'</span>)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> api == <span class="string">'random'</span>:</span><br><span class="line">            result = self.redis.random()</span><br><span class="line">            <span class="keyword">if</span> result:</span><br><span class="line">                self.write(result)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> api == <span class="string">'names'</span>:</span><br><span class="line">            result = self.redis.names()</span><br><span class="line">            <span class="keyword">if</span> result:</span><br><span class="line">                self.write(json.dumps(result))</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> api == <span class="string">'proxies'</span>:</span><br><span class="line">            result = self.redis.proxies()</span><br><span class="line">            <span class="keyword">if</span> result:</span><br><span class="line">                self.write(json.dumps(result))</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> api == <span class="string">'all'</span>:</span><br><span class="line">            result = self.redis.all()</span><br><span class="line">            <span class="keyword">if</span> result:</span><br><span class="line">                self.write(json.dumps(result))</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> api == <span class="string">'count'</span>:</span><br><span class="line">            self.write(str(self.redis.count()))</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">server</span><span class="params">(redis, port=API_PORT, address=<span class="string">''</span>)</span>:</span></span><br><span class="line">    application = Application([(<span class="string">r'/'</span>, MainHandler, dict(redis=redis)),</span><br><span class="line">        (<span class="string">r'/(.*)'</span>, MainHandler, dict(redis=redis)),</span><br><span class="line">    ])</span><br><span class="line">    application.listen(port, address=address)</span><br><span class="line">    print(<span class="string">'ADSL API Listening on'</span>, port)</span><br><span class="line">    tornado.ioloop.IOLoop.instance().start()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里定义了 5 个接口，random 获取随机代理，names 获取主机列表，proxies 获取代理列表，all 获取代理映射，count 获取代理数量。 程序启动之后便会在 API_PORT 端口上运行 Web 服务，主页面如图 9-18 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-053047.jpg" alt=""> 图 9-18 主页面 访问 proxies 接口可以获得所有代理列表，如图 9-19 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-053052.jpg" alt=""> 图 9-19 代理列表 访问 random 接口可以获取随机可用代理，如图 9-20 所示。 <img src="https://qiniu.cuiqingcai.com/2019-10-20-053057.jpg" alt=""> 图 9-20 随机代理 我们只需将接口部署到服务器上，即可通过 Web 接口获取可用代理，获取方式和代理池类似。</p>
                  <h3 id="9-本节代码"><a href="#9-本节代码" class="headerlink" title="9. 本节代码"></a>9. 本节代码</h3>
                  <p>本节代码地址为：<a href="https://github.com/Python3WebSpider/AdslProxy" target="_blank" rel="noopener">https://github.com/Python3WebSpider/AdslProxy</a>。</p>
                  <h3 id="10-结语"><a href="#10-结语" class="headerlink" title="10. 结语"></a>10. 结语</h3>
                  <p>本节介绍了 ADSL 拨号代理的搭建过程。通过这种代理，我们可以无限次更换 IP，而且线路非常稳定，抓取效果好很多。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-10-20 13:36:03" itemprop="dateCreated datePublished" datetime="2019-10-20T13:36:03+08:00">2019-10-20</time>
                </span>
                <span id="/7842.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 9.4-ADSL 拨号代理" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>10k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>9 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/7820.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/7820.html" class="post-title-link" itemprop="url">来了来了！爬虫工程师最想要的手把手 JS 逆向系列教程！</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>爬虫是大数据时代不可或缺的数据获取手段，它是综合技术的应用体现。</p>
                  <p>​有取就有失，有攻就有防。开发者为了保护数据，不得已想出了很多办法来限制爬虫对数据的获取。WEB 网站的构成使得 JavaScript 成为了开发者阻挡爬虫的最佳选择。</p>
                  <p>作为一名爬虫工程师，解决目标网站设置的反爬虫手段是职责所在。大家遇到的问题都很相似：</p>
                  <blockquote>
                    <p>1、遇到加密的内容就无从下手，一片迷茫……！</p>
                    <p>2、会一点 JS 语法，能解一些简单的，但复杂的就不行了！</p>
                    <p>3、抠代码太繁杂了，根本不知道怎么办，一早上都定位不到函数入口！</p>
                    <p>4、混淆过后的代码，看得头都痛！</p>
                    <p>5、咦，这串加密的字符串怎么搞？</p>
                  </blockquote>
                  <p><img src="https://tva1.sinaimg.cn/large/006y8mN6gy1g83b4kc8cwj307g07uwet.jpg" alt=""></p>
                  <p>需求又如何呢？</p>
                  <p>爬虫工程师真的需要学习逆向吗？</p>
                  <p>在此之前我们在多个爬虫工程师群做了调查，投票结果如下：</p>
                  <p><img src="https://tva1.sinaimg.cn/large/006y8mN6gy1g83b74ney7j30ku112ad7.jpg" alt=""> <img src="https://tva1.sinaimg.cn/large/006y8mN6gy1g83b6m63t0j30ku112go7.jpg" alt=""></p>
                  <p><strong>在招聘方面</strong></p>
                  <p><img src="https://tva1.sinaimg.cn/large/006y8mN6gy1g83b80c3xpj30u0084t9i.jpg" alt=""> 很多岗位都要求有逆向或者解决反爬虫的能力 <img src="https://tva1.sinaimg.cn/large/006y8mN6gy1g83b8bg88cj30u006jt9d.jpg" alt=""> 甚至作为优先选择的条件 <img src="https://tva1.sinaimg.cn/large/006y8mN6gy1g83b8ltjhqj30u00gxmyv.jpg" alt=""></p>
                  <p><strong>显然，拥有逆向能力的爬虫工程师的职业等级会变得更高、团队地位更高，薪资自然也更高。</strong></p>
                  <p>本课程将主要从原理和技巧两个角度来为大家讲解 JavaScript 反爬虫绕过的相关知识。课程从反爬虫原理、工具介绍和使用、JavaScript 基础语法入手，结合常见的反爬虫现象及其绕过实战操作，帮助你掌握中级爬虫工程师必备的 JavaScript 逆向知识，向更高的职级迈进!</p>
                  <h3 id="你将从本课程中收获什么"><a href="#你将从本课程中收获什么" class="headerlink" title="你将从本课程中收获什么?"></a><strong>你将从本课程中收获什么?</strong></h3>
                  <p>序号</p>
                  <p>内容</p>
                  <p>重要程度</p>
                  <p>1</p>
                  <p>深入理解 JavaScript 反爬虫的根本原因</p>
                  <p>A+</p>
                  <p>2</p>
                  <p>了解工作中常用的 JavaScript 语法和知识</p>
                  <p>B</p>
                  <p>3</p>
                  <p>掌握各种逆向神器的基本使用和骚操作</p>
                  <p>A</p>
                  <p>4</p>
                  <p>深入理解 JavaScript 代码混淆的原理</p>
                  <p>A+</p>
                  <p>5</p>
                  <p>掌握 JavaScript 中常见的编码和加密方法</p>
                  <p>A</p>
                  <p>6</p>
                  <p>拥有快速定位加密代码位置的能力</p>
                  <p>A</p>
                  <p>7</p>
                  <p>轻松面对各种各样的加密字符串</p>
                  <p>A</p>
                  <p>学习案例的制作思路均来自实际网站在用的反爬虫手段，当你学习完整套课程后就可以独立面对前端反爬虫问题，平时的苦恼也将迎刃而解。</p>
                  <h3 id="课程大纲设计"><a href="#课程大纲设计" class="headerlink" title="课程大纲设计"></a>课程大纲设计</h3>
                  <p>序号</p>
                  <p>标签</p>
                  <p>课程标题</p>
                  <p>1</p>
                  <p>追根究底</p>
                  <p>探寻 JavaScript 反爬虫的根本原因</p>
                  <p>2</p>
                  <p>浮沙之上</p>
                  <p>课程中用到的 JavaScript 语法和知识</p>
                  <p>3</p>
                  <p>奇门遁甲</p>
                  <p>使用 Python 执行 JavaScript</p>
                  <p>4</p>
                  <p>蓄势待发</p>
                  <p>浏览器开发者工具的介绍和使用技巧</p>
                  <p>5</p>
                  <p>磨刀霍霍</p>
                  <p>抓包和拦截工具的介绍和使用技巧</p>
                  <p>6</p>
                  <p>初窥门径</p>
                  <p>阻挠爬虫工程师的无限 debug</p>
                  <p>7</p>
                  <p>火眼金睛</p>
                  <p>定位加密参数对应代码位置的方法</p>
                  <p>8</p>
                  <p>拨开云雾</p>
                  <p>代码混淆的原理</p>
                  <p>9</p>
                  <p>一击即中</p>
                  <p>处理代码混淆的方法</p>
                  <p>10</p>
                  <p>知己知彼</p>
                  <p>掌握常见的编码和加密</p>
                  <p>11</p>
                  <p>长枪直入</p>
                  <p>轻松解决反人类的混淆代码</p>
                  <p>12</p>
                  <p>一叶障目</p>
                  <p>服务端返回的神秘字符串</p>
                  <p>13</p>
                  <p>螳臂当车</p>
                  <p>解密！AES 并不是每次都奏效</p>
                  <p>14</p>
                  <p>插翅难逃</p>
                  <p>纵然 CSS 加身也难逃命运的安排</p>
                  <p>15</p>
                  <p>真假猴王</p>
                  <p>Base64 竟有如此威力</p>
                  <p>16</p>
                  <p>过眼云烟</p>
                  <p>历练半生 归来仍是少年</p>
                  <p>备注：具体开课时的目录有可能与现在的大纲存在差异，但改动不大。 这课程大纲预售放出来，我们都不怕别的机构抄，随便它们模仿。</p>
                  <h3 id="课程特色和内容制作团队介绍"><a href="#课程特色和内容制作团队介绍" class="headerlink" title="课程特色和内容制作团队介绍"></a>课程特色和内容制作团队介绍</h3>
                  <p>自研练习平台，不触碰法律红线、练习案例不会过期。</p>
                  <p>我们没有xx顶级讲师，也没有国外xx计算机硕士博士，不存在的。</p>
                  <p>我们只有一线爬虫工程师，讲最实用的内容，做最有效的练习。团队成员包括：崔庆才、韦世东、陈祥安、张冶青、唐轶飞、蔡晋、冯威、戴煌金、周子淇。</p>
                  <p>不像在线课平台，加群后讲师根本没空回答问题。我们会持续跟进与交流，制作良心内容，恶心营销狗?干的事我们才不会干。</p>
                  <p>团队成员有图书出版经历，例如崔庆才的 IT 畅销书《Python3 网络爬虫开发实战》、韦世东即将出版的《Python3 反爬虫原理与绕过实战》。</p>
                  <p>团队水平如何请大家自行斟酌，这里我就不吹嘘什么了。</p>
                  <p><img src="https://tva1.sinaimg.cn/large/006y8mN6gy1g83b920xd7j30sg0lcq51.jpg" alt=""></p>
                  <h3 id="预售活动"><a href="#预售活动" class="headerlink" title="预售活动"></a>预售活动</h3>
                  <p>课程在准备当中，现在开放预售。完整课程售价 399，预售 50 元抵正式课程 100 元，且可提前进入视频教程的微信交流群，甚至可以提出想看的内容，团队会酌情考虑加课。</p>
                  <p>预售目标为 1000 人，不足 1000 人预售取消，逐个退款。</p>
                  <p>预售成功后，开课之前可以申请退课退款，支持全额退款。</p>
                  <p>参与预售的朋友还可以参与开课前的营销活动。</p>
                  <p>未参与预售的朋友，只能在开课时按原价或活动价购买课程。</p>
                  <p>预售活动截止日期为 2019年10月31日。</p>
                  <p><img src="https://mmbiz.qpic.cn/mmbiz_gif/a0YItejHaTD9aGmhbl6ib32VXibhNicvNqrUWoUEzprvG69eFlbbNuslsrP6vqtpUWAFApeBQak4yYRPanEQXKjDA/640?wx_fmt=gif" alt=""></p>
                  <h3 id="开课时间"><a href="#开课时间" class="headerlink" title="开课时间"></a>开课时间</h3>
                  <p>课程在准备当中，预计 2 个月左右可看，最迟 2020年01月20日 你就能看到了！</p>
                  <p>不过可以肯定的是，报名人数越多，内容制作越快。</p>
                  <h3 id="如何报名"><a href="#如何报名" class="headerlink" title="如何报名"></a>如何报名</h3>
                  <p>添加微信号：Domfreez，或者扫描下方二维码，与夜幕韦世东聊一聊。 我为粉丝争取到了额外的 50 元优惠，预售期间内主动出示优惠码：GERMEY01 即可在活动基础上再减 50 元！ 相信我没错的，现在你只需要花 50 元预订，399 元的课程就抵扣了 150，正式开售的时候只需要再付 249 即可学习，从此前端反爬虫不再是烦恼！ <img src="https://tva1.sinaimg.cn/large/006y8mN6gy1g83b9pswf8j30iq0owq49.jpg" alt=""> 声明： 本次活动最终解释权归内容制作方夜幕团队所有。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/韦世东学算法和反爬虫" class="author" itemprop="url" rel="index">韦世东学算法和反爬虫</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-10-19 08:44:48" itemprop="dateCreated datePublished" datetime="2019-10-19T08:44:48+08:00">2019-10-19</time>
                </span>
                <span id="/7820.html" class="post-meta-item leancloud_visitors" data-flag-title="来了来了！爬虫工程师最想要的手把手 JS 逆向系列教程！" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>1.9k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>2 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/7778.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/7778.html" class="post-title-link" itemprop="url">JSON-RPC 2.0 的一些规范</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>本文为转载文章，旨在记录一些有用的知识点。</p>
                  <ul>
                    <li>作者: <a href="https://groups.google.com/forum/#!forum/json-rpc" target="_blank" rel="noopener">JSON-RPC 工作组</a>&lt; json-rpc@googlegroups.com &gt;</li>
                    <li>原文链接: <a href="http://www.jsonrpc.org/specification" target="_blank" rel="noopener">http://www.jsonrpc.org/specification</a></li>
                    <li>翻译: <a href="http://geekdream.com/" target="_blank" rel="noopener">leozvc</a> &lt; xxfs91@gmail.com &gt;</li>
                  </ul>
                  <h2 id="1-概述"><a href="#1-概述" class="headerlink" title="1. 概述"></a>1. 概述</h2>
                  <p>JSON-RPC 是一个无状态且轻量级的远程过程调用 (RPC) 协议。 本规范主要定义了一些数据结构及其相关的处理规则。它允许运行在基于 socket, http 等诸多不同消息传输环境的同一进程中。其使用 <a href="http://www.json.org/" target="_blank" rel="noopener">JSON</a>（<a href="http://www.ietf.org/rfc/rfc4627.txt" target="_blank" rel="noopener">RFC 4627</a>）作为数据格式。 它为简单而生！</p>
                  <h2 id="2-约定"><a href="#2-约定" class="headerlink" title="2. 约定"></a>2. 约定</h2>
                  <p>文档中关键字 “MUST”、”MUST NOT”、”REQUIRED”、”SHALL”、”SHALL NOT”、”SHOULD”、”SHOULD NOT”、”RECOMMENDED”、”MAY” 和 “OPTIONAL” 将在 <a href="http://www.ietf.org/rfc/rfc2119.txt" target="_blank" rel="noopener">RFC 2119</a> 中得到详细的解释及描述。 由于 JSON-RPC 使用 JSON，它具有与其相同的类型系统 (见 <a href="http://www.json.org" target="_blank" rel="noopener">http://www.json.org</a> 或 <a href="http://www.ietf.org/rfc/rfc4627.txt" target="_blank" rel="noopener">RFC 4627</a>)。JSON 可以表示四个基本类型 (String、Numbers、Booleans 和 Null) 和两个结构化类型 (Objects 和 Arrays)。 规范中，术语 “Primitive” 标记那 4 种原始类型，“Structured” 标记两种结构化类型。任何时候文档涉及 JSON 数据类型，第一个字母都必须大写：Object，Array，String，Number，Boolean，Null。包括 True 和 False 也要大写。 在客户端与任何被匹配到的服务端之间交换的所有成员名字应是区分大小写的。 函数、方法、过程都可以认为是可以互换的。 客户端被定义为请求对象的来源及响应对象的处理程序。 服务端被定义为响应对象的起源和请求对象的处理程序。 该规范的一种实现为可以轻而易举的填补这两个角色，即使是在同一时间，同一客户端或其他不相同的客户端。 该规范不涉及复杂层。</p>
                  <h2 id="3-兼容性"><a href="#3-兼容性" class="headerlink" title="3. 兼容性"></a>3. 兼容性</h2>
                  <p>JSON-RPC 2.0 的请求对象和响应对象可能无法在现用的 JSON-RPC 1.0 客户端或服务端工作，然而我们可以很容易在两个版本间区分出 2.0，总会包含一个成员命名为 “jsonrpc” 且值为 “2.0”， 而 1.0 版本是不包含的。大部分的 2.0 实现应该考虑尝试处理 1.0 的对象，即使不是对等的也应给其相关提示。</p>
                  <h2 id="4-请求对象"><a href="#4-请求对象" class="headerlink" title="4. 请求对象"></a>4. 请求对象</h2>
                  <p>发送一个请求对象至服务端代表一个 rpc 调用， 一个请求对象包含下列成员： <strong>jsonrpc</strong></p>
                  <blockquote>
                    <p>指定 JSON-RPC 协议版本的字符串，必须准确写为 “2.0”</p>
                  </blockquote>
                  <p><strong>method</strong></p>
                  <blockquote>
                    <p>包含所要调用方法名称的字符串，以 rpc 开头的方法名，用英文句号（U+002E or ASCII 46）连接的为预留给 rpc 内部的方法名及扩展名，且不能在其他地方使用。</p>
                  </blockquote>
                  <p><strong>params</strong></p>
                  <blockquote>
                    <p>调用方法所需要的结构化参数值，该成员参数可以被省略。</p>
                  </blockquote>
                  <p><strong>id</strong></p>
                  <blockquote>
                    <p>已建立客户端的唯一标识 id，值必须包含一个字符串、数值或 NULL 空值。如果不包含该成员则被认定为是一个通知。该值一般不为 NULL <a href="http://wiki.geekdream.com/Specification/json-rpc_2.0.html#id1" target="_blank" rel="noopener">[1]</a>，若为数值则不应该包含小数 <a href="http://wiki.geekdream.com/Specification/json-rpc_2.0.html#id2" target="_blank" rel="noopener">[2]</a>。</p>
                  </blockquote>
                  <p>服务端必须回答相同的值如果包含在响应对象。 这个成员用来两个对象之间的关联上下文。 [1] 在请求对象中不建议使用 NULL 作为 id 值，因为该规范将使用空值认定为未知 id 的请求。另外，由于 JSON-RPC 1.0 的通知使用了空值，这可能引起处理上的混淆。 [2] 使用小数是不确定性的，因为许多十进制小数不能精准的表达为二进制小数。</p>
                  <h3 id="4-1-通知"><a href="#4-1-通知" class="headerlink" title="4.1 通知"></a>4.1 通知</h3>
                  <p>没有包含 “id” 成员的请求对象为通知， 作为通知的请求对象表明客户端对相应的响应对象并不感兴趣，本身也没有响应对象需要返回给客户端。服务端必须不回复一个通知，包含那些批量请求中的。 由于通知没有返回的响应对象，所以通知不确定是否被定义。同样，客户端不会意识到任何错误（例如参数缺省，内部错误）。</p>
                  <h3 id="4-2-参数结构"><a href="#4-2-参数结构" class="headerlink" title="4.2 参数结构"></a>4.2 参数结构</h3>
                  <p>rpc 调用如果存在参数则必须为基本类型或结构化类型的参数值，要么为索引数组，要么为关联数组对象。</p>
                  <ul>
                    <li>索引：参数必须为数组，并包含与服务端预期顺序一致的参数值。</li>
                    <li>关联名称：参数必须为对象，并包含与服务端相匹配的参数成员名称。没有在预期中的成员名称可能会引起错误。名称必须完全匹配，包括方法的预期参数名以及大小写。</li>
                  </ul>
                  <h2 id="5-响应对象"><a href="#5-响应对象" class="headerlink" title="5. 响应对象"></a>5. 响应对象</h2>
                  <p>当发起一个 rpc 调用时，除通知之外，服务端都必须回复响应。响应表示为一个 JSON 对象，使用以下成员： <strong>jsonrpc</strong></p>
                  <blockquote>
                    <p>指定 JSON-RPC 协议版本的字符串，必须准确写为 “2.0”</p>
                  </blockquote>
                  <p><strong>result</strong></p>
                  <blockquote>
                    <p>该成员在成功时必须包含。 当调用方法引起错误时必须不包含该成员。 服务端中的被调用方法决定了该成员的值。</p>
                  </blockquote>
                  <p><strong>error</strong></p>
                  <blockquote>
                    <p>该成员在失败是必须包含。 当没有引起错误的时必须不包含该成员。 该成员参数值必须为 5.1 中定义的对象。</p>
                  </blockquote>
                  <p><strong>id</strong></p>
                  <blockquote>
                    <p>该成员必须包含。 该成员值必须于请求对象中的 id 成员值一致。 若在检查请求对象 id 时错误（例如参数错误或无效请求），则该值必须为空值。</p>
                  </blockquote>
                  <p>响应对象必须包含 result 或 error 成员，但两个成员必须不能同时包含。</p>
                  <h3 id="5-1-错误对象"><a href="#5-1-错误对象" class="headerlink" title="5.1 错误对象"></a>5.1 错误对象</h3>
                  <p>当一个 rpc 调用遇到错误时，返回的响应对象必须包含错误成员参数，并且为带有下列成员参数的对象： <strong>code</strong></p>
                  <blockquote>
                    <p>使用数值表示该异常的错误类型。 必须为整数。</p>
                  </blockquote>
                  <p><strong>message</strong></p>
                  <blockquote>
                    <p>对该错误的简单描述字符串。 该描述应尽量限定在简短的一句话。</p>
                  </blockquote>
                  <p><strong>data</strong></p>
                  <blockquote>
                    <p>包含关于错误附加信息的基本类型或结构化类型。该成员可忽略。 该成员值由服务端定义（例如详细的错误信息，嵌套的错误等）。</p>
                  </blockquote>
                  <p>-32768 至 - 32000 为保留的预定义错误代码。在该范围内的错误代码不能被明确定义，保留下列以供将来使用。错误代码基本与 XML-RPC 建议的一样，url： <a href="http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php" target="_blank" rel="noopener">http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php</a></p>
                  <p>code</p>
                  <p>message</p>
                  <p>meaning</p>
                  <p>-32700</p>
                  <p>Parse error 语法解析错误</p>
                  <p>服务端接收到无效的 json。该错误发送于服务器尝试解析 json 文本</p>
                  <p>-32600</p>
                  <p>Invalid Request 无效请求</p>
                  <p>发送的 json 不是一个有效的请求对象。</p>
                  <p>-32601</p>
                  <p>Method not found 找不到方法</p>
                  <p>该方法不存在或无效</p>
                  <p>-32602</p>
                  <p>Invalid params 无效的参数</p>
                  <p>无效的方法参数。</p>
                  <p>-32603</p>
                  <p>Internal error 内部错误</p>
                  <p>JSON-RPC 内部错误。</p>
                  <p>-32000 to -32099</p>
                  <p>Server error 服务端错误</p>
                  <p>预留用于自定义的服务器错误。</p>
                  <p>除此之外剩余的错误类型代码可供应用程序作为自定义错误。</p>
                  <h2 id="6-批量调用"><a href="#6-批量调用" class="headerlink" title="6. 批量调用"></a>6. 批量调用</h2>
                  <p>当需要同时发送多个请求对象时，客户端可以发送一个包含所有请求对象的数组。 当批量调用的所有请求对象处理完成时，服务端则需要返回一个包含相对应的响应对象数组。每个响应对象都应对应每个请求对象，除非是通知的请求对象。服务端可以并发的，以任意顺序和任意宽度的并行性来处理这些批量调用。 这些相应的响应对象可以任意顺序的包含在返回的数组中，而客户端应该是基于各个响应对象中的 id 成员来匹配对应的请求对象。 若批量调用的 rpc 操作本身非一个有效 json 或一个至少包含一个值的数组，则服务端返回的将单单是一个响应对象而非数组。若批量调用没有需要返回的响应对象，则服务端不需要返回任何结果且必须不能返回一个空数组给客户端。</p>
                  <h2 id="7-示例"><a href="#7-示例" class="headerlink" title="7. 示例"></a>7. 示例</h2>
                  <p><strong>语法:</strong></p>
                  <blockquote>
                    <p>--&gt; data sent to Server data sent to Client</p>
                  </blockquote>
                  <p>带索引数组参数的rpc调用:</p>
                  <figure class="highlight clean">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">\--&gt; &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"subtract"</span>, <span class="string">"params"</span>: [<span class="number">42</span>, <span class="number">23</span>], <span class="string">"id"</span>: <span class="number">1</span>&#125;</span><br><span class="line"> &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"result"</span>: <span class="number">19</span>, <span class="string">"id"</span>: <span class="number">1</span>&#125;</span><br><span class="line"></span><br><span class="line">--&gt; &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"subtract"</span>, <span class="string">"params"</span>: [<span class="number">23</span>, <span class="number">42</span>], <span class="string">"id"</span>: <span class="number">2</span>&#125;</span><br><span class="line"> &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"result"</span>: <span class="number">-19</span>, <span class="string">"id"</span>: <span class="number">2</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>带关联数组参数的rpc调用:</p>
                  <figure class="highlight clean">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">\--&gt; &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"subtract"</span>, <span class="string">"params"</span>: &#123;<span class="string">"subtrahend"</span>: <span class="number">23</span>, <span class="string">"minuend"</span>: <span class="number">42</span>&#125;, <span class="string">"id"</span>: <span class="number">3</span>&#125;</span><br><span class="line"> &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"result"</span>: <span class="number">19</span>, <span class="string">"id"</span>: <span class="number">3</span>&#125;</span><br><span class="line"></span><br><span class="line">--&gt; &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"subtract"</span>, <span class="string">"params"</span>: &#123;<span class="string">"minuend"</span>: <span class="number">42</span>, <span class="string">"subtrahend"</span>: <span class="number">23</span>&#125;, <span class="string">"id"</span>: <span class="number">4</span>&#125;</span><br><span class="line"> &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"result"</span>: <span class="number">19</span>, <span class="string">"id"</span>: <span class="number">4</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>通知:</p>
                  <figure class="highlight clean">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">\--&gt; &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"update"</span>, <span class="string">"params"</span>: [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>]&#125;</span><br><span class="line">--&gt; &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"foobar"</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>不包含调用方法的rpc调用:</p>
                  <figure class="highlight clean">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">\--&gt; &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"foobar"</span>, <span class="string">"id"</span>: <span class="string">"1"</span>&#125;</span><br><span class="line"> &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"error"</span>: &#123;<span class="string">"code"</span>: <span class="number">-32601</span>, <span class="string">"message"</span>: <span class="string">"Method not found"</span>&#125;, <span class="string">"id"</span>: <span class="string">"1"</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>包含无效json的rpc调用:</p>
                  <figure class="highlight awk">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">\--&gt; &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"foobar, "</span>params<span class="string">": "</span>ba<span class="string">r", "</span>baz]</span><br><span class="line"> &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"error"</span>: &#123;<span class="string">"code"</span>: -<span class="number">32700</span>, <span class="string">"message"</span>: <span class="string">"Parse error"</span>&#125;, <span class="string">"id"</span>: null&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>包含无效请求对象的rpc调用:</p>
                  <figure class="highlight livescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="string">\--&gt;</span> &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="number">1</span>, <span class="string">"params"</span>: <span class="string">"bar"</span>&#125;</span><br><span class="line"> &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"error"</span>: &#123;<span class="string">"code"</span>: -<span class="number">32600</span>, <span class="string">"message"</span>: <span class="string">"Invalid Request"</span>&#125;, <span class="string">"id"</span>: <span class="literal">null</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>包含无效json的rpc批量调用:</p>
                  <figure class="highlight livescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="string">\--&gt;</span> [</span><br><span class="line">        &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"sum"</span>, <span class="string">"params"</span>: [<span class="number">1</span>,<span class="number">2</span>,<span class="number">4</span>], <span class="string">"id"</span>: <span class="string">"1"</span>&#125;,</span><br><span class="line">        &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span></span><br><span class="line">    ]</span><br><span class="line"> &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"error"</span>: &#123;<span class="string">"code"</span>: -<span class="number">32700</span>, <span class="string">"message"</span>: <span class="string">"Parse error"</span>&#125;, <span class="string">"id"</span>: <span class="literal">null</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>包含空数组的rpc调用:</p>
                  <figure class="highlight livescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="string">\--&gt;</span> []</span><br><span class="line"> &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"error"</span>: &#123;<span class="string">"code"</span>: -<span class="number">32600</span>, <span class="string">"message"</span>: <span class="string">"Invalid Request"</span>&#125;, <span class="string">"id"</span>: <span class="literal">null</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>非空且无效的rpc批量调用:</p>
                  <figure class="highlight livescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="string">\--&gt;</span> [<span class="number">1</span>]</span><br><span class="line"> [</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"error"</span>: &#123;<span class="string">"code"</span>: -<span class="number">32600</span>, <span class="string">"message"</span>: <span class="string">"Invalid Request"</span>&#125;, <span class="string">"id"</span>: <span class="literal">null</span>&#125;</span><br><span class="line">    ]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>无效的rpc批量调用:</p>
                  <figure class="highlight livescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="string">\--&gt;</span> [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]</span><br><span class="line"> [</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"error"</span>: &#123;<span class="string">"code"</span>: -<span class="number">32600</span>, <span class="string">"message"</span>: <span class="string">"Invalid Request"</span>&#125;, <span class="string">"id"</span>: <span class="literal">null</span>&#125;,</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"error"</span>: &#123;<span class="string">"code"</span>: -<span class="number">32600</span>, <span class="string">"message"</span>: <span class="string">"Invalid Request"</span>&#125;, <span class="string">"id"</span>: <span class="literal">null</span>&#125;,</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"error"</span>: &#123;<span class="string">"code"</span>: -<span class="number">32600</span>, <span class="string">"message"</span>: <span class="string">"Invalid Request"</span>&#125;, <span class="string">"id"</span>: <span class="literal">null</span>&#125;</span><br><span class="line">    ]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>rpc批量调用:</p>
                  <figure class="highlight prolog">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">\--&gt; [</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"sum"</span>, <span class="string">"params"</span>: [<span class="number">1</span>,<span class="number">2</span>,<span class="number">4</span>], <span class="string">"id"</span>: <span class="string">"1"</span>&#125;,</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"notify_hello"</span>, <span class="string">"params"</span>: [<span class="number">7</span>]&#125;,</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"subtract"</span>, <span class="string">"params"</span>: [<span class="number">42</span>,<span class="number">23</span>], <span class="string">"id"</span>: <span class="string">"2"</span>&#125;,</span><br><span class="line">    &#123;<span class="string">"foo"</span>: <span class="string">"boo"</span>&#125;,</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"foo.get"</span>, <span class="string">"params"</span>: &#123;<span class="string">"name"</span>: <span class="string">"myself"</span>&#125;, <span class="string">"id"</span>: <span class="string">"5"</span>&#125;,</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"method"</span>: <span class="string">"get_data"</span>, <span class="string">"id"</span>: <span class="string">"9"</span>&#125; </span><br><span class="line">    ]</span><br><span class="line"> [</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"result"</span>: <span class="number">7</span>, <span class="string">"id"</span>: <span class="string">"1"</span>&#125;,</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"result"</span>: <span class="number">19</span>, <span class="string">"id"</span>: <span class="string">"2"</span>&#125;,</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"error"</span>: &#123;<span class="string">"code"</span>: <span class="number">-32600</span>, <span class="string">"message"</span>: <span class="string">"Invalid Request"</span>&#125;, <span class="string">"id"</span>: null&#125;,</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"error"</span>: &#123;<span class="string">"code"</span>: <span class="number">-32601</span>, <span class="string">"message"</span>: <span class="string">"Method not found"</span>&#125;, <span class="string">"id"</span>: <span class="string">"5"</span>&#125;,</span><br><span class="line">    &#123;<span class="string">"jsonrpc"</span>: <span class="string">"2.0"</span>, <span class="string">"result"</span>: [<span class="string">"hello"</span>, <span class="number">5</span>], <span class="string">"id"</span>: <span class="string">"9"</span>&#125;</span><br><span class="line">    ]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>所有都为通知的rpc批量调用:</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">\<span class="comment">--&gt; [</span></span><br><span class="line">    &#123;"jsonrpc": "2.0", "method": "notify_sum", "params": [<span class="number">1</span>,<span class="number">2</span>,<span class="number">4</span>]&#125;,</span><br><span class="line">    &#123;"jsonrpc": "2.0", "method": "notify_hello", "params": [<span class="number">7</span>]&#125;</span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"> //<span class="keyword">Nothing</span> <span class="keyword">is</span> returned <span class="keyword">for</span> <span class="keyword">all</span> notification batches</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h2 id="8-扩展"><a href="#8-扩展" class="headerlink" title="8. 扩展"></a>8. 扩展</h2>
                  <p>以 rpc 开头的方法名预留作为系统扩展，且必须不能用于其他地方。每个系统扩展都应该有相关规范文档，所有系统扩展都应是可选的。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-10-17 10:54:01" itemprop="dateCreated datePublished" datetime="2019-10-17T10:54:01+08:00">2019-10-17</time>
                </span>
                <span id="/7778.html" class="post-meta-item leancloud_visitors" data-flag-title="JSON-RPC 2.0 的一些规范" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>6.1k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>6 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/7776.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/7776.html" class="post-title-link" itemprop="url">Python 中更优雅的日志记录方案 loguru</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>在 Python 中，一般情况下我们可能直接用自带的 logging 模块来记录日志，包括我之前的时候也是一样。在使用时我们需要配置一些 Handler、Formatter 来进行一些处理，比如把日志输出到不同的位置，或者设置一个不同的输出格式，或者设置日志分块和备份。但其实个人感觉 logging 用起来其实并不是那么好用，其实主要还是配置较为繁琐。</p>
                  <h2 id="常见使用"><a href="#常见使用" class="headerlink" title="常见使用"></a>常见使用</h2>
                  <p>首先看看 logging 常见的解决方案吧，我一般会配置输出到文件、控制台和 Elasticsearch。输出到控制台就仅仅是方便直接查看的；输出到文件是方便直接存储，保留所有历史记录的备份；输出到 Elasticsearch，直接将 Elasticsearch 作为存储和分析的中心，使用 Kibana 可以非常方便地分析和查看运行情况。 所以在这里我基本会对 logging 做如下的封装写法：</p>
                  <figure class="highlight nix">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="built_in">import</span> logging</span><br><span class="line"><span class="built_in">import</span> sys</span><br><span class="line">from os <span class="built_in">import</span> makedirs</span><br><span class="line">from os.path <span class="built_in">import</span> dirname, exists</span><br><span class="line"></span><br><span class="line">from cmreslogging.handlers <span class="built_in">import</span> CMRESHandler</span><br><span class="line"></span><br><span class="line"><span class="attr">loggers</span> = &#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="attr">LOG_ENABLED</span> = True  <span class="comment"># 是否开启日志</span></span><br><span class="line"><span class="attr">LOG_TO_CONSOLE</span> = True  <span class="comment"># 是否输出到控制台</span></span><br><span class="line"><span class="attr">LOG_TO_FILE</span> = True  <span class="comment"># 是否输出到文件</span></span><br><span class="line"><span class="attr">LOG_TO_ES</span> = True  <span class="comment"># 是否输出到 Elasticsearch</span></span><br><span class="line"></span><br><span class="line"><span class="attr">LOG_PATH</span> = './runtime.log'  <span class="comment"># 日志文件路径</span></span><br><span class="line"><span class="attr">LOG_LEVEL</span> = 'DEBUG'  <span class="comment"># 日志级别</span></span><br><span class="line"><span class="attr">LOG_FORMAT</span> = '%(levelname)s - %(asctime)s - process: %(process)d - %(filename)s - %(name)s - %(lineno)d - %(module)s - %(message)s'  <span class="comment"># 每条日志输出格式</span></span><br><span class="line"><span class="attr">ELASTIC_SEARCH_HOST</span> = 'eshost'  <span class="comment"># Elasticsearch Host</span></span><br><span class="line"><span class="attr">ELASTIC_SEARCH_PORT</span> = <span class="number">9200</span>  <span class="comment"># Elasticsearch Port</span></span><br><span class="line"><span class="attr">ELASTIC_SEARCH_INDEX</span> = 'runtime'  <span class="comment"># Elasticsearch Index Name</span></span><br><span class="line"><span class="attr">APP_ENVIRONMENT</span> = 'dev'  <span class="comment"># 运行环境，如测试环境还是生产环境</span></span><br><span class="line"></span><br><span class="line">def get_logger(<span class="attr">name=None):</span></span><br><span class="line">    <span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">    get logger by name</span></span><br><span class="line"><span class="string">    :param name: name of logger</span></span><br><span class="line"><span class="string">    :return: logger</span></span><br><span class="line"><span class="string">    "</span><span class="string">""</span></span><br><span class="line">    global loggers</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> not name: <span class="attr">name</span> = __name__</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> loggers.get(name):</span><br><span class="line">        return loggers.get(name)</span><br><span class="line"></span><br><span class="line">    <span class="attr">logger</span> = logging.getLogger(name)</span><br><span class="line">    logger.setLevel(LOG_LEVEL)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 输出到控制台</span></span><br><span class="line">    <span class="keyword">if</span> LOG_ENABLED <span class="literal">and</span> LOG_TO_CONSOLE:</span><br><span class="line">        <span class="attr">stream_handler</span> = logging.StreamHandler(sys.stdout)</span><br><span class="line">        stream_handler.setLevel(<span class="attr">level=LOG_LEVEL)</span></span><br><span class="line">        <span class="attr">formatter</span> = logging.Formatter(LOG_FORMAT)</span><br><span class="line">        stream_handler.setFormatter(formatter)</span><br><span class="line">        logger.addHandler(stream_handler)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 输出到文件</span></span><br><span class="line">    <span class="keyword">if</span> LOG_ENABLED <span class="literal">and</span> LOG_TO_FILE:</span><br><span class="line">        <span class="comment"># 如果路径不存在，创建日志文件文件夹</span></span><br><span class="line">        <span class="attr">log_dir</span> = dirname(log_path)</span><br><span class="line">        <span class="keyword">if</span> not exists(log_dir): makedirs(log_dir)</span><br><span class="line">        <span class="comment"># 添加 FileHandler</span></span><br><span class="line">        <span class="attr">file_handler</span> = logging.FileHandler(log_path, <span class="attr">encoding='utf-8')</span></span><br><span class="line">        file_handler.setLevel(<span class="attr">level=LOG_LEVEL)</span></span><br><span class="line">        <span class="attr">formatter</span> = logging.Formatter(LOG_FORMAT)</span><br><span class="line">        file_handler.setFormatter(formatter)</span><br><span class="line">        logger.addHandler(file_handler)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 输出到 Elasticsearch</span></span><br><span class="line">    <span class="keyword">if</span> LOG_ENABLED <span class="literal">and</span> LOG_TO_ES:</span><br><span class="line">        <span class="comment"># 添加 CMRESHandler</span></span><br><span class="line">        <span class="attr">es_handler</span> = CMRESHandler(<span class="attr">hosts=[&#123;'host':</span> ELASTIC_SEARCH_HOST, 'port': ELASTIC_SEARCH_PORT&#125;],</span><br><span class="line">                                  <span class="comment"># 可以配置对应的认证权限</span></span><br><span class="line">                                  <span class="attr">auth_type=CMRESHandler.AuthType.NO_AUTH,</span>  </span><br><span class="line">                                  <span class="attr">es_index_name=ELASTIC_SEARCH_INDEX,</span></span><br><span class="line">                                  <span class="comment"># 一个月分一个 Index</span></span><br><span class="line">                                  <span class="attr">index_name_frequency=CMRESHandler.IndexNameFrequency.MONTHLY,</span></span><br><span class="line">                                  <span class="comment"># 额外增加环境标识</span></span><br><span class="line">                                  <span class="attr">es_additional_fields=&#123;'environment':</span> APP_ENVIRONMENT&#125;  </span><br><span class="line">                                  )</span><br><span class="line">        es_handler.setLevel(<span class="attr">level=LOG_LEVEL)</span></span><br><span class="line">        <span class="attr">formatter</span> = logging.Formatter(LOG_FORMAT)</span><br><span class="line">        es_handler.setFormatter(formatter)</span><br><span class="line">        logger.addHandler(es_handler)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 保存到全局 loggers</span></span><br><span class="line">    loggers[name] = logger</span><br><span class="line">    return logger</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>定义完了怎么使用呢？只需要使用定义的方法获取一个 logger，然后 log 对应的内容即可：</p>
                  <figure class="highlight gcode">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">logger = get_logger<span class="comment">()</span></span><br><span class="line">logger.debug<span class="comment">('this is a message')</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果如下：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">DEBUG - <span class="number">2019</span><span class="number">-10</span><span class="number">-11</span> <span class="number">22</span>:<span class="number">27</span>:<span class="number">35</span>,<span class="number">923</span> - process: <span class="number">99490</span> - logger.py - __main__ - <span class="number">81</span> - logger - <span class="keyword">this</span> <span class="keyword">is</span> a message</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们看看这个定义的基本实现吧。首先这里一些常量是用来定义 logging 模块的一些基本属性的，比如 <code>LOG_ENABLED</code> 代表是否开启日志功能，<code>LOG_TO_ES</code> 代表是否将日志输出到 Elasticsearch，另外还有很多其他的日志基本配置，如 <code>LOG_FORMAT</code> 配置了日志每个条目输出的基本格式，另外还有一些连接的必要信息。这些变量可以和运行时的命令行或环境变量对接起来，可以方便地实现一些开关和配置的更换。 然后定义了这么一个 get_logger 方法，接收一个参数 name。首先该方法拿到 name 之后，会到全局的 loggers 变量里面查找，loggers 变量是一个全局字典，如果有已经声明过的 logger，直接将其获取返回即可，不用再将其二次初始化。如果 loggers 里面没有找到 name 对应的 logger，那就进行创建即可。创建 logger 之后，可以为其添加各种对应的 Handler，如输出到控制台就用 StreamHandler，输出到文件就用 FileHandler 或 RotatingFileHandler，输出到 Elasticsearch 就用 CMRESHandler，分别配置好对应的信息即可。 最后呢，将新建的 logger 保存到全局的 loggers 里面并返回即可，这样如果有同名的 logger 便可以直接查找 loggers 直接返回了。 在这里依赖了额外的输出到 Elasticsearch 的包，叫做 CMRESHandler，它可以支持将日志输出到 Elasticsearch 里面，如果要使用的话可以安装一下：</p>
                  <figure class="highlight cmake">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">pip <span class="keyword">install</span> CMRESHandler</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>其 GitHub 地址是：<a href="https://github.com/cmanaha/python-elasticsearch-logger，具体的使用方式可以看看它的官方说明，如配置认证信息，配置" target="_blank" rel="noopener">https://github.com/cmanaha/python-elasticsearch-logger，具体的使用方式可以看看它的官方说明，如配置认证信息，配置</a> Index 分隔信息等等。 好，上面就是我之前常用的 logging 配置，通过如上的配置，我就可以实现将 logging 输出到三个位置，并可以实现对应的效果。比如输出到 Elasticsearch 之后，我就可以非常方便地使用 Kibana 来查看当前运行情况，ERROR Log 的比例等等，如图所示： <img src="https://qiniu.cuiqingcai.com/2019-10-13-143457.png" alt=""> 也可以在它的基础上做更进一步的统计分析。</p>
                  <h2 id="loguru"><a href="#loguru" class="headerlink" title="loguru"></a>loguru</h2>
                  <p>上面的实现方式已经是一个较为可行的配置方案了。然而，我还是会感觉到有些 Handler 配起来麻烦，尤其是新建一个项目的很多时候懒得去写一些配置。即使是不用上文的配置，用最基本的几行 logging 配置，像如下的通用配置：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">import logging</span><br><span class="line">logging.basicConfig(level = logging.INFO,format = <span class="string">'%(asctime)s - %(name)s - %(levelname)s - %(message)s'</span>)</span><br><span class="line">logger = logging.getLogger(__name__)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我也懒得去写，感觉并不是一个优雅的实现方式。 有需求就有动力啊，这不，就有人实现了这么一个库，叫做 loguru，可以将 log 的配置和使用更加简单和方便。 下面我们来看看它到底是怎么用的吧。</p>
                  <h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2>
                  <p>首先，这个库的安装方式很简单，就用基本的 pip 安装即可，Python 3 版本的安装如下：</p>
                  <figure class="highlight cmake">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">pip3 <span class="keyword">install</span> loguru</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>安装完毕之后，我们就可以在项目里使用这个 loguru 库了。</p>
                  <h2 id="基本使用"><a href="#基本使用" class="headerlink" title="基本使用"></a>基本使用</h2>
                  <p>那么这个库怎么来用呢？我们先用一个实例感受下：</p>
                  <figure class="highlight moonscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> loguru <span class="keyword">import</span> logger</span><br><span class="line"></span><br><span class="line">logger.<span class="built_in">debug</span>(<span class="string">'this is a debug message'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>看到了吧，不需要配置什么东西，直接引入一个 logger，然后调用其 debug 方法即可。 在 loguru 里面有且仅有一个主要对象，那就是 logger，loguru 里面有且仅有一个 logger，而且它已经被提前配置了一些基础信息，比如比较友好的格式化、文本颜色信息等等。 上面的代码运行结果如下：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">2019</span><span class="number">-10</span><span class="number">-13</span> <span class="number">22</span>:<span class="number">46</span>:<span class="number">12.367</span> | DEBUG    | __main__:&lt;module&gt;:<span class="number">4</span> - <span class="keyword">this</span> <span class="keyword">is</span> a debug message</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以看到其默认的输出格式是上面的内容，有时间、级别、模块名、行号以及日志信息，不需要手动创建 logger，直接使用即可，另外其输出还是彩色的，看起来会更加友好。 以上的日志信息是直接输出到控制台的，并没有输出到其他的地方，如果想要输出到其他的位置，比如存为文件，我们只需要使用一行代码声明即可。 例如将结果同时输出到一个 runtime.log 文件里面，可以这么写：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> loguru import logger</span><br><span class="line"></span><br><span class="line">logger.<span class="builtin-name">add</span>(<span class="string">'runtime.log'</span>)</span><br><span class="line">logger.<span class="builtin-name">debug</span>(<span class="string">'this is a debug'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>很简单吧，我们也不需要再声明一个 FileHandler 了，就一行 add 语句搞定，运行之后会发现目录下 runtime.log 里面同样出现了刚刚控制台输出的 DEBUG 信息。 上面就是一些基本的使用，但这还远远不够，下面我们来详细了解下它的一些功能模块。</p>
                  <h2 id="详细使用"><a href="#详细使用" class="headerlink" title="详细使用"></a>详细使用</h2>
                  <p>既然是日志，那么最常见的就是输出到文件了。loguru 对输出到文件的配置有非常强大的支持，比如支持输出到多个文件，分级别分别输出，过大创建新文件，过久自动删除等等。 下面我们分别看看这些怎样来实现，这里基本上就是 add 方法的使用介绍。因为这个 add 方法就相当于给 logger 添加了一个 Handler，它给我们暴露了许多参数来实现 Handler 的配置，下面我们来详细介绍下。 首先看看它的方法定义吧：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def <span class="builtin-name">add</span>(</span><br><span class="line">        self,</span><br><span class="line">        sink,</span><br><span class="line">        *,</span><br><span class="line">        <span class="attribute">level</span>=_defaults.LOGURU_LEVEL,</span><br><span class="line">        <span class="attribute">format</span>=_defaults.LOGURU_FORMAT,</span><br><span class="line">        <span class="attribute">filter</span>=_defaults.LOGURU_FILTER,</span><br><span class="line">        <span class="attribute">colorize</span>=_defaults.LOGURU_COLORIZE,</span><br><span class="line">        <span class="attribute">serialize</span>=_defaults.LOGURU_SERIALIZE,</span><br><span class="line">        <span class="attribute">backtrace</span>=_defaults.LOGURU_BACKTRACE,</span><br><span class="line">        <span class="attribute">diagnose</span>=_defaults.LOGURU_DIAGNOSE,</span><br><span class="line">        <span class="attribute">enqueue</span>=_defaults.LOGURU_ENQUEUE,</span><br><span class="line">        <span class="attribute">catch</span>=_defaults.LOGURU_CATCH,</span><br><span class="line">        **kwargs</span><br><span class="line">    ):</span><br><span class="line">    pass</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>看看它的源代码，它支持这么多的参数，如 level、format、filter、color 等等，另外我们还注意到它有个非常重要的参数 sink，我们看看官方文档：<a href="https://loguru.readthedocs.io/en/stable/api/logger.html#sink，可以了解到通过" target="_blank" rel="noopener">https://loguru.readthedocs.io/en/stable/api/logger.html#sink，可以了解到通过</a> sink 我们可以传入多种不同的数据结构，汇总如下：</p>
                  <ul>
                    <li>sink 可以传入一个 file 对象，例如 <code>sys.stderr</code> 或者 <code>open(&#39;file.log&#39;, &#39;w&#39;)</code> 都可以。</li>
                    <li>sink 可以直接传入一个 <code>str</code> 字符串或者 <code>pathlib.Path</code> 对象，其实就是代表文件路径的，如果识别到是这种类型，它会自动创建对应路径的日志文件并将日志输出进去。</li>
                    <li>sink 可以是一个方法，可以自行定义输出实现。</li>
                    <li>sink 可以是一个 logging 模块的 Handler，比如 FileHandler、StreamHandler 等等，或者上文中我们提到的 CMRESHandler 照样也是可以的，这样就可以实现自定义 Handler 的配置。</li>
                    <li>sink 还可以是一个自定义的类，具体的实现规范可以参见官方文档。</li>
                  </ul>
                  <p>所以说，刚才我们所演示的输出到文件，仅仅给它传了一个 str 字符串路径，他就给我们创建了一个日志文件，就是这个原理。</p>
                  <h3 id="基本参数"><a href="#基本参数" class="headerlink" title="基本参数"></a>基本参数</h3>
                  <p>下面我们再了解下它的其他参数，例如 format、filter、level 等等。 其实它们的概念和格式和 logging 模块都是基本一样的了，例如这里使用 format、filter、level 来规定输出的格式：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">logger.<span class="builtin-name">add</span>(<span class="string">'runtime.log'</span>, <span class="attribute">format</span>=<span class="string">"&#123;time&#125; &#123;level&#125; &#123;message&#125;"</span>, <span class="attribute">filter</span>=<span class="string">"my_module"</span>, <span class="attribute">level</span>=<span class="string">"INFO"</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h3 id="删除-sink"><a href="#删除-sink" class="headerlink" title="删除 sink"></a>删除 sink</h3>
                  <p>另外添加 sink 之后我们也可以对其进行删除，相当于重新刷新并写入新的内容。 删除的时候根据刚刚 add 方法返回的 id 进行删除即可，看下面的例子：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> loguru import logger</span><br><span class="line"></span><br><span class="line">trace = logger.<span class="builtin-name">add</span>(<span class="string">'runtime.log'</span>)</span><br><span class="line">logger.<span class="builtin-name">debug</span>(<span class="string">'this is a debug message'</span>)</span><br><span class="line">logger.<span class="builtin-name">remove</span>(trace)</span><br><span class="line">logger.<span class="builtin-name">debug</span>(<span class="string">'this is another debug message'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>看这里，我们首先 add 了一个 sink，然后获取它的返回值，赋值为 trace。随后输出了一条日志，然后将 trace 变量传给 remove 方法，再次输出一条日志，看看结果是怎样的。 控制台输出如下：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">2019</span><span class="number">-10</span><span class="number">-13</span> <span class="number">23</span>:<span class="number">18</span>:<span class="number">26.469</span> | DEBUG    | __main__:&lt;module&gt;:<span class="number">4</span> - <span class="keyword">this</span> <span class="keyword">is</span> a debug message</span><br><span class="line"><span class="number">2019</span><span class="number">-10</span><span class="number">-13</span> <span class="number">23</span>:<span class="number">18</span>:<span class="number">26.469</span> | DEBUG    | __main__:&lt;module&gt;:<span class="number">6</span> - <span class="keyword">this</span> <span class="keyword">is</span> another debug message</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>日志文件 runtime.log 内容如下：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">2019</span><span class="number">-10</span><span class="number">-13</span> <span class="number">23</span>:<span class="number">18</span>:<span class="number">26.469</span> | DEBUG    | __main__:&lt;module&gt;:<span class="number">4</span> - <span class="keyword">this</span> <span class="keyword">is</span> a debug message</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以发现，在调用 remove 方法之后，确实将历史 log 删除了。但实际上这并不是删除，只不过是将 sink 对象移除之后，在这之前的内容不会再输出到日志中。 这样我们就可以实现日志的刷新重新写入操作。</p>
                  <h3 id="rotation-配置"><a href="#rotation-配置" class="headerlink" title="rotation 配置"></a>rotation 配置</h3>
                  <p>用了 loguru 我们还可以非常方便地使用 rotation 配置，比如我们想一天输出一个日志文件，或者文件太大了自动分隔日志文件，我们可以直接使用 add 方法的 rotation 参数进行配置。 我们看看下面的例子：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">logger.<span class="builtin-name">add</span>(<span class="string">'runtime_&#123;time&#125;.log'</span>, <span class="attribute">rotation</span>=<span class="string">"500 MB"</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>通过这样的配置我们就可以实现每 500MB 存储一个文件，每个 log 文件过大就会新创建一个 log 文件。我们在配置 log 名字时加上了一个 time 占位符，这样在生成时可以自动将时间替换进去，生成一个文件名包含时间的 log 文件。 另外我们也可以使用 rotation 参数实现定时创建 log 文件，例如：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">logger.<span class="builtin-name">add</span>(<span class="string">'runtime_&#123;time&#125;.log'</span>, <span class="attribute">rotation</span>=<span class="string">'00:00'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样就可以实现每天 0 点新创建一个 log 文件输出了。 另外我们也可以配置 log 文件的循环时间，比如每隔一周创建一个 log 文件，写法如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">logger.<span class="builtin-name">add</span>(<span class="string">'runtime_&#123;time&#125;.log'</span>, <span class="attribute">rotation</span>=<span class="string">'1 week'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我们就可以实现一周创建一个 log 文件了。</p>
                  <h3 id="retention-配置"><a href="#retention-配置" class="headerlink" title="retention 配置"></a>retention 配置</h3>
                  <p>很多情况下，一些非常久远的 log 对我们来说并没有什么用处了，它白白占据了一些存储空间，不清除掉就会非常浪费。retention 这个参数可以配置日志的最长保留时间。 比如我们想要设置日志文件最长保留 10 天，可以这么来配置：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">logger.<span class="builtin-name">add</span>(<span class="string">'runtime.log'</span>, <span class="attribute">retention</span>=<span class="string">'10 days'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样 log 文件里面就会保留最新 10 天的 log，妈妈再也不用担心 log 沉积的问题啦。</p>
                  <h3 id="compression-配置"><a href="#compression-配置" class="headerlink" title="compression 配置"></a>compression 配置</h3>
                  <p>loguru 还可以配置文件的压缩格式，比如使用 zip 文件格式保存，示例如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">logger.<span class="builtin-name">add</span>(<span class="string">'runtime.log'</span>, <span class="attribute">compression</span>=<span class="string">'zip'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样可以更加节省存储空间。</p>
                  <h3 id="字符串格式化"><a href="#字符串格式化" class="headerlink" title="字符串格式化"></a>字符串格式化</h3>
                  <p>loguru 在输出 log 的时候还提供了非常友好的字符串格式化功能，像这样：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">logger.<span class="builtin-name">info</span>(<span class="string">'If you are using Python &#123;&#125;, prefer &#123;feature&#125; of course!'</span>, 3.6, <span class="attribute">feature</span>=<span class="string">'f-strings'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样在添加参数就非常方便了。</p>
                  <h3 id="Traceback-记录"><a href="#Traceback-记录" class="headerlink" title="Traceback 记录"></a>Traceback 记录</h3>
                  <p>在很多情况下，如果遇到运行错误，而我们在打印输出 log 的时候万一不小心没有配置好 Traceback 的输出，很有可能我们就没法追踪错误所在了。 但用了 loguru 之后，我们用它提供的装饰器就可以直接进行 Traceback 的记录，类似这样的配置即可：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">@logger.catch</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">my_function</span><span class="params">(x, y, z)</span></span><span class="symbol">:</span></span><br><span class="line">    <span class="comment"># An error? It's caught anyway!</span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span> / (x + y + z)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们做个测试，我们在调用时三个参数都传入 0，直接引发除以 0 的错误，看看会出现什么情况：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">my_function(<span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行完毕之后，可以发现 log 里面就出现了 Traceback 信息，而且给我们输出了当时的变量值，真的是不能再赞了！结果如下：</p>
                  <figure class="highlight yaml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="string">\&gt;</span> <span class="string">File</span> <span class="string">"run.py"</span><span class="string">,</span> <span class="string">line</span> <span class="number">15</span><span class="string">,</span> <span class="string">in</span> <span class="string">&lt;module&gt;</span></span><br><span class="line">    <span class="string">my_function(0,</span> <span class="number">0</span><span class="string">,</span> <span class="number">0</span><span class="string">)</span></span><br><span class="line">    <span class="string">└</span> <span class="string">&lt;function</span> <span class="string">my_function</span> <span class="string">at</span> <span class="number">0x1171dd510</span><span class="string">&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="string">File</span> <span class="string">"/private/var/py/logurutest/demo5.py"</span><span class="string">,</span> <span class="string">line</span> <span class="number">13</span><span class="string">,</span> <span class="string">in</span> <span class="string">my_function</span></span><br><span class="line">    <span class="string">return</span> <span class="number">1</span> <span class="string">/</span> <span class="string">(x</span> <span class="string">+</span> <span class="string">y</span> <span class="string">+</span> <span class="string">z)</span></span><br><span class="line">                <span class="string">│</span>   <span class="string">│</span>   <span class="string">└</span> <span class="number">0</span></span><br><span class="line">                <span class="string">│</span>   <span class="string">└</span> <span class="number">0</span></span><br><span class="line">                <span class="string">└</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line"><span class="attr">ZeroDivisionError:</span> <span class="string">division</span> <span class="string">by</span> <span class="string">zero</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>因此，用 loguru 可以非常方便地实现日志追踪，debug 效率可能要高上十倍了？ 另外 loguru 还有很多很多强大的功能，这里就不再一一展开讲解了，更多的内容大家可以看看 loguru 的官方文档详细了解一下：<a href="https://loguru.readthedocs.io/en/stable/index.html" target="_blank" rel="noopener">https://loguru.readthedocs.io/en/stable/index.html</a>。 看完之后，是时候把自己的 logging 模块替换成 loguru 啦！</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-10-17 10:52:49" itemprop="dateCreated datePublished" datetime="2019-10-17T10:52:49+08:00">2019-10-17</time>
                </span>
                <span id="/7776.html" class="post-meta-item leancloud_visitors" data-flag-title="Python 中更优雅的日志记录方案 loguru" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>8.5k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>8 分钟</span>
                </span>
              </div>
            </article>
            <script>
              document.querySelectorAll('.random').forEach(item => item.src = "https://picsum.photos/id/" + Math.floor(Math.random() * Math.floor(300)) + "/200/133")

            </script>
            <nav class="pagination">
              <a class="extend prev" rel="prev" href="/page/10/"><i class="fa fa-angle-left" aria-label="上一页"></i></a><a class="page-number" href="/">1</a><span class="space">&hellip;</span><a class="page-number" href="/page/10/">10</a><span class="page-number current">11</span><a class="page-number" href="/page/12/">12</a><span class="space">&hellip;</span><a class="page-number" href="/page/31/">31</a><a class="extend next" rel="next" href="/page/12/"><i class="fa fa-angle-right" aria-label="下一页"></i></a>
            </nav>
          </div>
          <script>
            window.addEventListener('tabs:register', () =>
            {
              let
              {
                activeClass
              } = CONFIG.comments;
              if (CONFIG.comments.storage)
              {
                activeClass = localStorage.getItem('comments_active') || activeClass;
              }
              if (activeClass)
              {
                let activeTab = document.querySelector(`a[href="#comment-${activeClass}"]`);
                if (activeTab)
                {
                  activeTab.click();
                }
              }
            });
            if (CONFIG.comments.storage)
            {
              window.addEventListener('tabs:click', event =>
              {
                if (!event.target.matches('.tabs-comment .tab-content .tab-pane')) return;
                let commentClass = event.target.classList[1];
                localStorage.setItem('comments_active', commentClass);
              });
            }

          </script>
        </div>
        <div class="toggle sidebar-toggle">
          <span class="toggle-line toggle-line-first"></span>
          <span class="toggle-line toggle-line-middle"></span>
          <span class="toggle-line toggle-line-last"></span>
        </div>
        <aside class="sidebar">
          <div class="sidebar-inner">
            <ul class="sidebar-nav motion-element">
              <li class="sidebar-nav-toc"> 文章目录 </li>
              <li class="sidebar-nav-overview"> 站点概览 </li>
            </ul>
            <!--noindex-->
            <div class="post-toc-wrap sidebar-panel">
            </div>
            <!--/noindex-->
            <div class="site-overview-wrap sidebar-panel">
              <div class="site-author motion-element" itemprop="author" itemscope itemtype="http://schema.org/Person">
                <img class="site-author-image" itemprop="image" alt="崔庆才" src="/images/avatar.png">
                <p class="site-author-name" itemprop="name">崔庆才</p>
                <div class="site-description" itemprop="description">崔庆才的个人站点，记录生活的瞬间，分享学习的心得。</div>
              </div>
              <div class="site-state-wrap motion-element">
                <nav class="site-state">
                  <div class="site-state-item site-state-posts">
                    <a href="/archives/">
                      <span class="site-state-item-count">608</span>
                      <span class="site-state-item-name">日志</span>
                    </a>
                  </div>
                  <div class="site-state-item site-state-categories">
                    <a href="/categories/">
                      <span class="site-state-item-count">24</span>
                      <span class="site-state-item-name">分类</span></a>
                  </div>
                  <div class="site-state-item site-state-tags">
                    <a href="/tags/">
                      <span class="site-state-item-count">156</span>
                      <span class="site-state-item-name">标签</span></a>
                  </div>
                </nav>
              </div>
              <div class="links-of-author motion-element">
                <span class="links-of-author-item">
                  <a href="https://github.com/Germey" title="GitHub → https:&#x2F;&#x2F;github.com&#x2F;Germey" rel="noopener" target="_blank"><i class="fab fa-github fa-fw"></i>GitHub</a>
                </span>
                <span class="links-of-author-item">
                  <a href="mailto:cqc@cuiqingcai.com.com" title="邮件 → mailto:cqc@cuiqingcai.com.com" rel="noopener" target="_blank"><i class="fa fa-envelope fa-fw"></i>邮件</a>
                </span>
                <span class="links-of-author-item">
                  <a href="https://weibo.com/cuiqingcai" title="微博 → https:&#x2F;&#x2F;weibo.com&#x2F;cuiqingcai" rel="noopener" target="_blank"><i class="fab fa-weibo fa-fw"></i>微博</a>
                </span>
                <span class="links-of-author-item">
                  <a href="https://www.zhihu.com/people/Germey" title="知乎 → https:&#x2F;&#x2F;www.zhihu.com&#x2F;people&#x2F;Germey" rel="noopener" target="_blank"><i class="fa fa-magic fa-fw"></i>知乎</a>
                </span>
              </div>
            </div>
            <div style=" width: 100%;" class="sidebar-panel sidebar-panel-image sidebar-panel-active">
              <a href="https://tutorial.lengyue.video/?coupon=12ef4b1a-a3db-11ea-bb37-0242ac130002_cqx_850" target="_blank" rel="noopener">
                <img src="https://qiniu.cuiqingcai.com/bco2a.png" style=" width: 100%;">
              </a>
            </div>
            <div style=" width: 100%;" class="sidebar-panel sidebar-panel-image sidebar-panel-active">
              <a href="http://www.ipidea.net/?utm-source=cqc&utm-keyword=?cqc" target="_blank" rel="noopener">
                <img src="https://qiniu.cuiqingcai.com/0ywun.png" style=" width: 100%;">
              </a>
            </div>
            <div class="sidebar-panel sidebar-panel-tags sidebar-panel-active">
              <h4 class="name"> 标签云 </h4>
              <div class="content">
                <a href="/tags/2048/" style="font-size: 10px;">2048</a> <a href="/tags/API/" style="font-size: 10px;">API</a> <a href="/tags/Bootstrap/" style="font-size: 11.25px;">Bootstrap</a> <a href="/tags/CDN/" style="font-size: 10px;">CDN</a> <a href="/tags/CQC/" style="font-size: 10px;">CQC</a> <a href="/tags/CSS/" style="font-size: 10px;">CSS</a> <a href="/tags/CSS-%E5%8F%8D%E7%88%AC%E8%99%AB/" style="font-size: 10px;">CSS 反爬虫</a> <a href="/tags/CV/" style="font-size: 10px;">CV</a> <a href="/tags/Django/" style="font-size: 10px;">Django</a> <a href="/tags/Eclipse/" style="font-size: 11.25px;">Eclipse</a> <a href="/tags/FTP/" style="font-size: 10px;">FTP</a> <a href="/tags/Git/" style="font-size: 10px;">Git</a> <a href="/tags/GitHub/" style="font-size: 13.75px;">GitHub</a> <a href="/tags/HTML5/" style="font-size: 10px;">HTML5</a> <a href="/tags/Hexo/" style="font-size: 10px;">Hexo</a> <a href="/tags/IT/" style="font-size: 10px;">IT</a> <a href="/tags/JSP/" style="font-size: 10px;">JSP</a> <a href="/tags/JavaScript/" style="font-size: 10px;">JavaScript</a> <a href="/tags/K8s/" style="font-size: 10px;">K8s</a> <a href="/tags/LOGO/" style="font-size: 10px;">LOGO</a> <a href="/tags/Linux/" style="font-size: 10px;">Linux</a> <a href="/tags/MIUI/" style="font-size: 10px;">MIUI</a> <a href="/tags/MongoDB/" style="font-size: 10px;">MongoDB</a> <a href="/tags/Mysql/" style="font-size: 10px;">Mysql</a> <a href="/tags/NBA/" style="font-size: 10px;">NBA</a> <a href="/tags/PHP/" style="font-size: 11.25px;">PHP</a> <a href="/tags/PS/" style="font-size: 10px;">PS</a> <a href="/tags/Pathlib/" style="font-size: 10px;">Pathlib</a> <a href="/tags/PhantomJS/" style="font-size: 10px;">PhantomJS</a> <a href="/tags/Python/" style="font-size: 15px;">Python</a> <a href="/tags/Python3/" style="font-size: 12.5px;">Python3</a> <a href="/tags/Pythonic/" style="font-size: 10px;">Pythonic</a> <a href="/tags/QQ/" style="font-size: 10px;">QQ</a> <a href="/tags/Redis/" style="font-size: 10px;">Redis</a> <a href="/tags/SAE/" style="font-size: 10px;">SAE</a> <a href="/tags/SSH/" style="font-size: 10px;">SSH</a> <a href="/tags/SVG/" style="font-size: 10px;">SVG</a> <a href="/tags/Scrapy/" style="font-size: 10px;">Scrapy</a> <a href="/tags/Scrapy-redis/" style="font-size: 10px;">Scrapy-redis</a> <a href="/tags/Scrapy%E5%88%86%E5%B8%83%E5%BC%8F/" style="font-size: 10px;">Scrapy分布式</a> <a href="/tags/Selenium/" style="font-size: 10px;">Selenium</a> <a href="/tags/TKE/" style="font-size: 10px;">TKE</a> <a href="/tags/Ubuntu/" style="font-size: 11.25px;">Ubuntu</a> <a href="/tags/VS-Code/" style="font-size: 10px;">VS Code</a> <a href="/tags/Vs-Code/" style="font-size: 10px;">Vs Code</a> <a href="/tags/Vue/" style="font-size: 11.25px;">Vue</a> <a href="/tags/Webpack/" style="font-size: 10px;">Webpack</a> <a href="/tags/Windows/" style="font-size: 10px;">Windows</a> <a href="/tags/Winpcap/" style="font-size: 10px;">Winpcap</a> <a href="/tags/WordPress/" style="font-size: 13.75px;">WordPress</a> <a href="/tags/Youtube/" style="font-size: 11.25px;">Youtube</a> <a href="/tags/android/" style="font-size: 10px;">android</a> <a href="/tags/ansible/" style="font-size: 10px;">ansible</a> <a href="/tags/cocos2d-x/" style="font-size: 10px;">cocos2d-x</a> <a href="/tags/e6/" style="font-size: 10px;">e6</a> <a href="/tags/fitvids/" style="font-size: 10px;">fitvids</a> <a href="/tags/git/" style="font-size: 11.25px;">git</a> <a href="/tags/json/" style="font-size: 10px;">json</a> <a href="/tags/js%E9%80%86%E5%90%91/" style="font-size: 10px;">js逆向</a> <a href="/tags/kubernetes/" style="font-size: 10px;">kubernetes</a> <a href="/tags/log/" style="font-size: 10px;">log</a> <a href="/tags/logging/" style="font-size: 10px;">logging</a> <a href="/tags/matlab/" style="font-size: 11.25px;">matlab</a> <a href="/tags/python/" style="font-size: 20px;">python</a> <a href="/tags/pytube/" style="font-size: 11.25px;">pytube</a> <a href="/tags/pywin32/" style="font-size: 10px;">pywin32</a> <a href="/tags/style/" style="font-size: 10px;">style</a> <a href="/tags/tomcat/" style="font-size: 10px;">tomcat</a> <a href="/tags/ubuntu/" style="font-size: 10px;">ubuntu</a> <a href="/tags/uwsgi/" style="font-size: 10px;">uwsgi</a> <a href="/tags/vsftpd/" style="font-size: 10px;">vsftpd</a> <a href="/tags/wamp/" style="font-size: 10px;">wamp</a> <a href="/tags/wineQQ/" style="font-size: 10px;">wineQQ</a> <a href="/tags/%E4%B8%83%E7%89%9B/" style="font-size: 11.25px;">七牛</a> <a href="/tags/%E4%B8%8A%E6%B5%B7/" style="font-size: 10px;">上海</a> <a href="/tags/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/" style="font-size: 10px;">个人网站</a> <a href="/tags/%E4%B8%BB%E9%A2%98/" style="font-size: 10px;">主题</a> <a href="/tags/%E4%BA%91%E4%BA%A7%E5%93%81/" style="font-size: 10px;">云产品</a> <a href="/tags/%E4%BA%91%E5%AD%98%E5%82%A8/" style="font-size: 10px;">云存储</a> <a href="/tags/%E4%BA%AC%E4%B8%9C%E4%BA%91/" style="font-size: 10px;">京东云</a> <a href="/tags/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/" style="font-size: 12.5px;">人工智能</a> <a href="/tags/%E4%BB%A3%E7%90%86/" style="font-size: 10px;">代理</a> <a href="/tags/%E4%BB%A3%E7%A0%81/" style="font-size: 10px;">代码</a> <a href="/tags/%E4%BB%A3%E7%A0%81%E5%88%86%E4%BA%AB%E5%9B%BE/" style="font-size: 10px;">代码分享图</a> <a href="/tags/%E4%BC%98%E5%8C%96/" style="font-size: 10px;">优化</a> <a href="/tags/%E4%BD%8D%E8%BF%90%E7%AE%97/" style="font-size: 10px;">位运算</a> <a href="/tags/%E5%85%AC%E4%BC%97%E5%8F%B7/" style="font-size: 10px;">公众号</a> <a href="/tags/%E5%88%86%E4%BA%AB/" style="font-size: 10px;">分享</a> <a href="/tags/%E5%88%86%E5%B8%83%E5%BC%8F/" style="font-size: 10px;">分布式</a> <a href="/tags/%E5%88%9B%E4%B8%9A/" style="font-size: 10px;">创业</a> <a href="/tags/%E5%89%8D%E7%AB%AF/" style="font-size: 12.5px;">前端</a> <a href="/tags/%E5%8D%9A%E5%AE%A2/" style="font-size: 10px;">博客</a> <a href="/tags/%E5%8E%9F%E7%94%9FAPP/" style="font-size: 10px;">原生APP</a> <a href="/tags/%E5%8F%8D%E7%88%AC%E8%99%AB/" style="font-size: 12.5px;">反爬虫</a> <a href="/tags/%E5%91%BD%E4%BB%A4/" style="font-size: 10px;">命令</a> <a href="/tags/%E5%93%8D%E5%BA%94%E5%BC%8F%E5%B8%83%E5%B1%80/" style="font-size: 10px;">响应式布局</a> <a href="/tags/%E5%9E%83%E5%9C%BE%E9%82%AE%E4%BB%B6/" style="font-size: 10px;">垃圾邮件</a> <a href="/tags/%E5%9F%9F%E5%90%8D%E7%BB%91%E5%AE%9A/" style="font-size: 10px;">域名绑定</a> <a href="/tags/%E5%A4%8D%E7%9B%98/" style="font-size: 10px;">复盘</a> <a href="/tags/%E5%A4%A7%E4%BC%97%E7%82%B9%E8%AF%84/" style="font-size: 10px;">大众点评</a> <a href="/tags/%E5%AD%97%E4%BD%93%E5%8F%8D%E7%88%AC%E8%99%AB/" style="font-size: 10px;">字体反爬虫</a> <a href="/tags/%E5%AD%97%E7%AC%A6%E9%97%AE%E9%A2%98/" style="font-size: 10px;">字符问题</a> <a href="/tags/%E5%AD%A6%E4%B9%A0%E6%96%B9%E6%B3%95/" style="font-size: 10px;">学习方法</a> <a href="/tags/%E5%AE%89%E5%8D%93/" style="font-size: 10px;">安卓</a> <a href="/tags/%E5%AE%9E%E7%94%A8/" style="font-size: 10px;">实用</a> <a href="/tags/%E5%B0%81%E9%9D%A2/" style="font-size: 10px;">封面</a> <a href="/tags/%E5%B4%94%E5%BA%86%E6%89%8D/" style="font-size: 18.75px;">崔庆才</a> <a href="/tags/%E5%B7%A5%E5%85%B7/" style="font-size: 12.5px;">工具</a> <a href="/tags/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/" style="font-size: 10px;">开发工具</a> <a href="/tags/%E5%BE%AE%E8%BD%AF/" style="font-size: 10px;">微软</a> <a href="/tags/%E6%80%9D%E8%80%83/" style="font-size: 10px;">思考</a> <a href="/tags/%E6%89%8B%E6%9C%BA%E8%AE%BF%E9%97%AE/" style="font-size: 10px;">手机访问</a> <a href="/tags/%E6%95%99%E7%A8%8B/" style="font-size: 10px;">教程</a> <a href="/tags/%E6%95%99%E8%82%B2/" style="font-size: 10px;">教育</a> <a href="/tags/%E6%96%B0%E4%B9%A6/" style="font-size: 12.5px;">新书</a> <a href="/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/" style="font-size: 10px;">方法论</a> <a href="/tags/%E6%97%85%E6%B8%B8/" style="font-size: 10px;">旅游</a> <a href="/tags/%E6%97%A5%E5%BF%97/" style="font-size: 10px;">日志</a> <a href="/tags/%E6%9A%97%E6%97%B6%E9%97%B4/" style="font-size: 10px;">暗时间</a> <a href="/tags/%E6%9D%9C%E5%85%B0%E7%89%B9/" style="font-size: 11.25px;">杜兰特</a> <a href="/tags/%E6%A1%8C%E9%9D%A2/" style="font-size: 10px;">桌面</a> <a href="/tags/%E6%AD%8C%E5%8D%95/" style="font-size: 10px;">歌单</a> <a href="/tags/%E6%B1%9F%E5%8D%97/" style="font-size: 10px;">江南</a> <a href="/tags/%E6%B8%B8%E6%88%8F/" style="font-size: 10px;">游戏</a> <a href="/tags/%E7%84%A6%E8%99%91/" style="font-size: 10px;">焦虑</a> <a href="/tags/%E7%88%AC%E8%99%AB/" style="font-size: 16.25px;">爬虫</a> <a href="/tags/%E7%88%AC%E8%99%AB%E4%B9%A6%E7%B1%8D/" style="font-size: 11.25px;">爬虫书籍</a> <a href="/tags/%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F/" style="font-size: 10px;">环境变量</a> <a href="/tags/%E7%94%9F%E6%B4%BB%E7%AC%94%E8%AE%B0/" style="font-size: 10px;">生活笔记</a> <a href="/tags/%E7%99%BB%E5%BD%95/" style="font-size: 10px;">登录</a> <a href="/tags/%E7%9F%A5%E4%B9%8E/" style="font-size: 10px;">知乎</a> <a href="/tags/%E7%9F%AD%E4%BF%A1/" style="font-size: 10px;">短信</a> <a href="/tags/%E7%9F%AD%E4%BF%A1%E9%AA%8C%E8%AF%81%E7%A0%81/" style="font-size: 10px;">短信验证码</a> <a href="/tags/%E7%AC%94%E8%AE%B0%E8%BD%AF%E4%BB%B6/" style="font-size: 10px;">笔记软件</a> <a href="/tags/%E7%AF%AE%E7%BD%91/" style="font-size: 10px;">篮网</a> <a href="/tags/%E7%BA%B8%E5%BC%A0/" style="font-size: 10px;">纸张</a> <a href="/tags/%E7%BB%84%E4%BB%B6/" style="font-size: 10px;">组件</a> <a href="/tags/%E7%BD%91%E7%AB%99/" style="font-size: 10px;">网站</a> <a href="/tags/%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB/" style="font-size: 11.25px;">网络爬虫</a> <a href="/tags/%E7%BE%8E%E5%AD%A6/" style="font-size: 10px;">美学</a> <a href="/tags/%E8%82%89%E5%A4%B9%E9%A6%8D/" style="font-size: 10px;">肉夹馍</a> <a href="/tags/%E8%85%BE%E8%AE%AF%E4%BA%91/" style="font-size: 10px;">腾讯云</a> <a href="/tags/%E8%87%AA%E5%BE%8B/" style="font-size: 10px;">自律</a> <a href="/tags/%E8%A5%BF%E5%B0%91%E7%88%B7/" style="font-size: 10px;">西少爷</a> <a href="/tags/%E8%A7%86%E9%A2%91/" style="font-size: 10px;">视频</a> <a href="/tags/%E8%B0%B7%E6%AD%8C%E9%AA%8C%E8%AF%81%E7%A0%81/" style="font-size: 10px;">谷歌验证码</a> <a href="/tags/%E8%BF%90%E8%90%A5/" style="font-size: 10px;">运营</a> <a href="/tags/%E8%BF%9C%E7%A8%8B/" style="font-size: 10px;">远程</a> <a href="/tags/%E9%80%86%E5%90%91/" style="font-size: 10px;">逆向</a> <a href="/tags/%E9%85%8D%E7%BD%AE/" style="font-size: 10px;">配置</a> <a href="/tags/%E9%87%8D%E8%A3%85/" style="font-size: 10px;">重装</a> <a href="/tags/%E9%98%BF%E6%9D%9C/" style="font-size: 10px;">阿杜</a> <a href="/tags/%E9%9D%99%E8%A7%85/" style="font-size: 17.5px;">静觅</a> <a href="/tags/%E9%A2%A0%E8%A6%86/" style="font-size: 10px;">颠覆</a> <a href="/tags/%E9%A3%9E%E4%BF%A1/" style="font-size: 10px;">飞信</a> <a href="/tags/%E9%B8%BF%E8%92%99/" style="font-size: 10px;">鸿蒙</a>
              </div>
              <script>
                const tagsColors = ['#00a67c', '#5cb85c', '#d9534f', '#567e95', '#b37333', '#f4843d', '#15a287']
                const tagsElements = document.querySelectorAll('.sidebar-panel-tags .content a')
                tagsElements.forEach((item) =>
                {
                  item.style.backgroundColor = tagsColors[Math.floor(Math.random() * tagsColors.length)]
                })

              </script>
            </div>
            <div class="sidebar-panel sidebar-panel-categories sidebar-panel-active">
              <h4 class="name"> 分类 </h4>
              <div class="content">
                <ul class="category-list">
                  <li class="category-list-item"><a class="category-list-link" href="/categories/C-C/">C/C++</a><span class="category-list-count">23</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/HTML/">HTML</a><span class="category-list-count">14</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Java/">Java</a><span class="category-list-count">5</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/JavaScript/">JavaScript</a><span class="category-list-count">26</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Linux/">Linux</a><span class="category-list-count">15</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Markdown/">Markdown</a><span class="category-list-count">1</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Net/">Net</a><span class="category-list-count">4</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Other/">Other</a><span class="category-list-count">39</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/PHP/">PHP</a><span class="category-list-count">27</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Paper/">Paper</a><span class="category-list-count">2</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Python/">Python</a><span class="category-list-count">261</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/TypeScript/">TypeScript</a><span class="category-list-count">2</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E4%B8%AA%E4%BA%BA%E5%B1%95%E7%A4%BA/">个人展示</a><span class="category-list-count">1</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E4%B8%AA%E4%BA%BA%E6%97%A5%E8%AE%B0/">个人日记</a><span class="category-list-count">9</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E4%B8%AA%E4%BA%BA%E8%AE%B0%E5%BD%95/">个人记录</a><span class="category-list-count">4</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E4%B8%AA%E4%BA%BA%E9%9A%8F%E7%AC%94/">个人随笔</a><span class="category-list-count">15</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E5%AE%89%E8%A3%85%E9%85%8D%E7%BD%AE/">安装配置</a><span class="category-list-count">59</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/">技术杂谈</a><span class="category-list-count">88</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E6%9C%AA%E5%88%86%E7%B1%BB/">未分类</a><span class="category-list-count">1</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E7%94%9F%E6%B4%BB%E7%AC%94%E8%AE%B0/">生活笔记</a><span class="category-list-count">1</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E7%A6%8F%E5%88%A9%E4%B8%93%E5%8C%BA/">福利专区</a><span class="category-list-count">6</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E8%81%8C%E4%BD%8D%E6%8E%A8%E8%8D%90/">职位推荐</a><span class="category-list-count">2</span></li>
                </ul>
              </div>
            </div>
            <div class="sidebar-panel sidebar-panel-friends sidebar-panel-active">
              <h4 class="name"> 友情链接 </h4>
              <ul class="friends">
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/j2dub.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.findhao.net/" target="_blank" rel="noopener">FindHao</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/ou6mm.jpg">
                  </span>
                  <span class="link">
                    <a href="https://diygod.me/" target="_blank" rel="noopener">DIYgod</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/6apxu.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.51dev.com/" target="_blank" rel="noopener">IT技术社区</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://www.jankl.com/img/titleshu.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.jankl.com/" target="_blank" rel="noopener">liberalist</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/bqlbs.png">
                  </span>
                  <span class="link">
                    <a href="http://www.urselect.com/" target="_blank" rel="noopener">优社电商</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/8s88c.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.yuanrenxue.com/" target="_blank" rel="noopener">猿人学</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/2wgg5.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.yunlifang.cn/" target="_blank" rel="noopener">云立方</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/shwr6.png">
                  </span>
                  <span class="link">
                    <a href="http://lanbing510.info/" target="_blank" rel="noopener">冰蓝</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/blvoh.jpg">
                  </span>
                  <span class="link">
                    <a href="https://lengyue.me/" target="_blank" rel="noopener">冷月</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="http://qianxunclub.com/favicon.png">
                  </span>
                  <span class="link">
                    <a href="http://qianxunclub.com/" target="_blank" rel="noopener">千寻啊千寻</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/0044u.jpg">
                  </span>
                  <span class="link">
                    <a href="http://kodcloud.com/" target="_blank" rel="noopener">可道云</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/ygnpn.jpg">
                  </span>
                  <span class="link">
                    <a href="http://www.kunkundashen.cn/" target="_blank" rel="noopener">坤坤大神</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/22uv1.png">
                  </span>
                  <span class="link">
                    <a href="http://www.cenchong.com/" target="_blank" rel="noopener">岑冲博客</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/ev9kl.png">
                  </span>
                  <span class="link">
                    <a href="http://www.zxiaoji.com/" target="_blank" rel="noopener">张小鸡</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://www.503error.com/favicon.ico">
                  </span>
                  <span class="link">
                    <a href="https://www.503error.com/" target="_blank" rel="noopener">张志明个人博客</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/x714o.jpg">
                  </span>
                  <span class="link">
                    <a href="http://www.hubwiz.com/" target="_blank" rel="noopener">汇智网</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/129d8.png">
                  </span>
                  <span class="link">
                    <a href="https://www.bysocket.com/" target="_blank" rel="noopener">泥瓦匠BYSocket</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://www.xiongge.club/favicon.ico">
                  </span>
                  <span class="link">
                    <a href="https://www.xiongge.club/" target="_blank" rel="noopener">熊哥club</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/3w4fe.png">
                  </span>
                  <span class="link">
                    <a href="https://zerlong.com/" target="_blank" rel="noopener">知语</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/44hxf.png">
                  </span>
                  <span class="link">
                    <a href="http://redstonewill.com/" target="_blank" rel="noopener">红色石头</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/8g1fk.jpg">
                  </span>
                  <span class="link">
                    <a href="http://www.laodong.me/" target="_blank" rel="noopener">老董博客</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/wkaus.jpg">
                  </span>
                  <span class="link">
                    <a href="https://zhaoshuai.me/" target="_blank" rel="noopener">碎念</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/pgo0r.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.chenwenguan.com/" target="_blank" rel="noopener">陈文管的博客</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/kk82a.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.lxlinux.net/" target="_blank" rel="noopener">良许Linux教程网</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/lj0t2.jpg">
                  </span>
                  <span class="link">
                    <a href="https://tanqingbo.cn/" target="_blank" rel="noopener">IT码农</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/i8cdr.png">
                  </span>
                  <span class="link">
                    <a href="https://junyiseo.com/" target="_blank" rel="noopener">均益个人博客</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/chwv2.png">
                  </span>
                  <span class="link">
                    <a href="https://brucedone.com/" target="_blank" rel="noopener">大鱼的鱼塘</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/2y43o.png">
                  </span>
                  <span class="link">
                    <a href="http://bbs.nightteam.cn/" target="_blank" rel="noopener">夜幕爬虫安全论坛</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/zvc3w.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.weishidong.com/" target="_blank" rel="noopener">韦世东的技术专栏</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/ebudy.jpg">
                  </span>
                  <span class="link">
                    <a href="https://chuanjiabing.com/" target="_blank" rel="noopener">穿甲兵技术社区</a>
                  </span>
                </li>
              </ul>
            </div>
          </div>
        </aside>
        <div id="sidebar-dimmer"></div>
      </div>
    </main>
    <footer class="footer">
      <div class="footer-inner">
        <div class="copyright"> &copy; <span itemprop="copyrightYear">2021</span>
          <span class="with-love">
            <i class="fa fa-heart"></i>
          </span>
          <span class="author" itemprop="copyrightHolder">崔庆才丨静觅</span>
          <span class="post-meta-divider">|</span>
          <span class="post-meta-item-icon">
            <i class="fa fa-chart-area"></i>
          </span>
          <span title="站点总字数">2.6m</span>
          <span class="post-meta-divider">|</span>
          <span class="post-meta-item-icon">
            <i class="fa fa-coffee"></i>
          </span>
          <span title="站点阅读时长">39:54</span>
        </div>
        <div class="powered-by">由 <a href="https://hexo.io/" class="theme-link" rel="noopener" target="_blank">Hexo</a> & <a href="https://pisces.theme-next.org/" class="theme-link" rel="noopener" target="_blank">NexT.Pisces</a> 强力驱动 </div>
        <div class="beian"><a href="https://beian.miit.gov.cn/" rel="noopener" target="_blank">京ICP备18015597号-1 </a>
        </div>
        <script>
          (function ()
          {
            function leancloudSelector(url)
            {
              url = encodeURI(url);
              return document.getElementById(url).querySelector('.leancloud-visitors-count');
            }

            function addCount(Counter)
            {
              var visitors = document.querySelector('.leancloud_visitors');
              var url = decodeURI(visitors.id);
              var title = visitors.dataset.flagTitle;
              Counter('get', '/classes/Counter?where=' + encodeURIComponent(JSON.stringify(
              {
                url
              }))).then(response => response.json()).then((
              {
                results
              }) =>
              {
                if (results.length > 0)
                {
                  var counter = results[0];
                  leancloudSelector(url).innerText = counter.time + 1;
                  Counter('put', '/classes/Counter/' + counter.objectId,
                  {
                    time:
                    {
                      '__op': 'Increment',
                      'amount': 1
                    }
                  }).catch(error =>
                  {
                    console.error('Failed to save visitor count', error);
                  });
                }
                else
                {
                  Counter('post', '/classes/Counter',
                  {
                    title,
                    url,
                    time: 1
                  }).then(response => response.json()).then(() =>
                  {
                    leancloudSelector(url).innerText = 1;
                  }).catch(error =>
                  {
                    console.error('Failed to create', error);
                  });
                }
              }).catch(error =>
              {
                console.error('LeanCloud Counter Error', error);
              });
            }

            function showTime(Counter)
            {
              var visitors = document.querySelectorAll('.leancloud_visitors');
              var entries = [...visitors].map(element =>
              {
                return decodeURI(element.id);
              });
              Counter('get', '/classes/Counter?where=' + encodeURIComponent(JSON.stringify(
              {
                url:
                {
                  '$in': entries
                }
              }))).then(response => response.json()).then((
              {
                results
              }) =>
              {
                for (let url of entries)
                {
                  let target = results.find(item => item.url === url);
                  leancloudSelector(url).innerText = target ? target.time : 0;
                }
              }).catch(error =>
              {
                console.error('LeanCloud Counter Error', error);
              });
            }
            let
            {
              app_id,
              app_key,
              server_url
            } = {
              "enable": true,
              "app_id": "6X5dRQ0pnPWJgYy8SXOg0uID-gzGzoHsz",
              "app_key": "ziLDVEy73ne5HtFTiGstzHMS",
              "server_url": "https://6x5drq0p.lc-cn-n1-shared.com",
              "security": false
            };

            function fetchData(api_server)
            {
              var Counter = (method, url, data) =>
              {
                return fetch(`${api_server}/1.1${url}`,
                {
                  method,
                  headers:
                  {
                    'X-LC-Id': app_id,
                    'X-LC-Key': app_key,
                    'Content-Type': 'application/json',
                  },
                  body: JSON.stringify(data)
                });
              };
              if (CONFIG.page.isPost)
              {
                if (CONFIG.hostname !== location.hostname) return;
                addCount(Counter);
              }
              else if (document.querySelectorAll('.post-title-link').length >= 1)
              {
                showTime(Counter);
              }
            }
            let api_server = app_id.slice(-9) !== '-MdYXbMMI' ? server_url : `https://${app_id.slice(0, 8).toLowerCase()}.api.lncldglobal.com`;
            if (api_server)
            {
              fetchData(api_server);
            }
            else
            {
              fetch('https://app-router.leancloud.cn/2/route?appId=' + app_id).then(response => response.json()).then((
              {
                api_server
              }) =>
              {
                fetchData('https://' + api_server);
              });
            }
          })();

        </script>
      </div>
      <div class="footer-stat">
        <span id="cnzz_stat_icon_1279355174"></span>
        <script type="text/javascript">
          document.write(unescape("%3Cspan id='cnzz_stat_icon_1279355174'%3E%3C/span%3E%3Cscript src='https://v1.cnzz.com/z_stat.php%3Fid%3D1279355174%26online%3D1%26show%3Dline' type='text/javascript'%3E%3C/script%3E"));

        </script>
      </div>
    </footer>
  </div>
  <script src="//cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/pangu@4/dist/browser/pangu.min.js"></script>
  <script src="/js/utils.js"></script>
  <script src="/.js"></script>
  <script src="/js/schemes/pisces.js"></script>
  <script src="/.js"></script>
  <script src="/js/next-boot.js"></script>
  <script src="/.js"></script>
  <script>
    (function ()
    {
      var canonicalURL, curProtocol;
      //Get the <link> tag
      var x = document.getElementsByTagName("link");
      //Find the last canonical URL
      if (x.length > 0)
      {
        for (i = 0; i < x.length; i++)
        {
          if (x[i].rel.toLowerCase() == 'canonical' && x[i].href)
          {
            canonicalURL = x[i].href;
          }
        }
      }
      //Get protocol
      if (!canonicalURL)
      {
        curProtocol = window.location.protocol.split(':')[0];
      }
      else
      {
        curProtocol = canonicalURL.split(':')[0];
      }
      //Get current URL if the canonical URL does not exist
      if (!canonicalURL) canonicalURL = window.location.href;
      //Assign script content. Replace current URL with the canonical URL
      ! function ()
      {
        var e = /([http|https]:\/\/[a-zA-Z0-9\_\.]+\.baidu\.com)/gi,
          r = canonicalURL,
          t = document.referrer;
        if (!e.test(r))
        {
          var n = (String(curProtocol).toLowerCase() === 'https') ? "https://sp0.baidu.com/9_Q4simg2RQJ8t7jm9iCKT-xh_/s.gif" : "//api.share.baidu.com/s.gif";
          t ? (n += "?r=" + encodeURIComponent(document.referrer), r && (n += "&l=" + r)) : r && (n += "?l=" + r);
          var i = new Image;
          i.src = n
        }
      }(window);
    })();

  </script>
  <script src="/js/local-search.js"></script>
  <script src="/.js"></script>
</body>

</html>
